Frage

I have an Entity class with a variety of subclasses for particular types of entities. The objects of the subclasses have various relationships with each other, so I was using Entity pointers and static_casts to describe these relationships and to allow entities to access information about the other entities they have relationships with.

However, I found that using raw pointers was a source of a lot of difficulty & hard to debug errors. For instance, I wasted several hours before realizing I was making pointers to an object before copying it into a vector, which invalidated the pointers.

I'm using vectors and lists & stl containers like that to manage all of my memory - I am really trying to avoid worrying about memory management and low-level issues.

Say Cow is a subclass of Entity, and I have a vector of Cows (or some data structure of Cows). This vector might move itself around in memory if it needs to resize or something. I don't really want to care about those details, but I know they might invalidate my pointers. So if a Pig has a relationship with a cow and the cow vector is resized, the Pig loses track of where its friend is located in memory & has a dangerous incorrect pointer.

Here are my questions, although I think someone experienced might be able to help me more just by reading the situation...

  1. Should I try to slip something into the constructor/destructor of my Entity objects so that they automatically invalidate relationships other entities have with them when they are deleted or update memory locations if they are moved? If this is a good approach, any help on the semantics would be useful. I have been writing all of my classes with no constructors or destructors at all so that they are extremely easy to use in things like vectors.
  2. Is the better option to just keep track of the containers & manually update the pointers whenever I know the containers are going to move? So the strategy there would be to iterate through any container that may have moved right after it moves and update all of the pointers immediately.
War es hilfreich?

Lösung

As I understand it, you're doing something like this:

cow florence, buttercup;
std::vector<cow> v = { florence, buttercup };
cow* buttercup_ptr = &v[1];

// do something involving the cow*

And your buttercup_ptr is getting invalidated because, eg. florence was removed from the vector.

It is possible to resolve this by using smart pointers, like this:

std::shared_ptr<cow> florence = std::make_shared<cow>();
std::vector<std::shared_ptr<cow>> v;
v.push_back(florence);

You can now freely share florence... regardles off how the vector gets shuffled around, it remains valid.

If you want to let florence be destroyed when you pop her out of the vector, that presents a minor problem: anyone holding a copy of the shared_ptr will prevent this particular cow instance being cleaned up. You can avoid that by using a weak_ptr.

std::weak_ptr<cow> florence_ref = florence;

In order to use a weak_ptr you call lock() on it to convert it to a full shared_ptr. If the underlying cow instance was already destroyed because it had no strong references, an excetion is throw (you can also call expired() to check this before lock(), of course!)

The other possibility (as I suggested in my comment on the original question) is to use iterators pointing into the container holding the cow instances. This is a little stranger, but iterators are quite like pointers in many ways. You just have to be careful in your choice of container, to ensure your iterators aren't going to be invalidate (which mean syou can't use a vector for this purpose);

std::set<cow> s;
cow florence;
auto iter = s.insert(florence);

// do something with iter, and not worry if other cows are added to
// or removed from s

I don't think I'd necessarily advise this option, though I'm sure it would work. Using smart pointers is a better way to show future maintainance programmers your intent and has the advantage that getting dangling references is much harder.

Andere Tipps

You have a misconception about pointers. A pointer is (for the sake of this discussion!) just an address. Why would copying an address invalidate anything? Let's say your object is created in memory at address 12345. new therefore gives you a pointer whose value is 12345:

Entity *entity = new Cow; // object created at address 12345
// entity now has value 12345

Later, when you copy that pointer into a vector (or any other standard container), you copy nothing but the value 12345.

std::vector<Entity *> entities;
entities.push_back(entity);
// entities[0] now has value 12345, entity does not change and remains 12345

In fact, there's no difference with regards to this compared to, say, a std::vector<int>:

int i = 12345;
std::vector<int> ints;
ints.push_back(i);
// ints[0] now has value 12345, i does not change and remains 12345

When the vector is copied, the values inside do not change, either:

std::vector<int> ints2 = ints;
std::vector<Entity *> entities2 = entities;
// i, ints[0] and ints[0] are 12345
// entity, entities[0] and entities[0] are 12345

So if a Pig has a relationship with a cow and the cow vector is resized, the Pig loses track of where its friend is located in memory & has a dangerous incorrect pointer.

This fear is unfounded. The Pig does not lose track of its friend.

The dangerous thing is keeping a pointer to a deleted object:

delete entity;
entities[0]->doSometing(); // undefined behaviour, may crash

How to solve this problem is subject of thousands of questions, websites, blogs and books in C++. Smart pointers may help you, but you should first truly understand the concept of ownership.

My two cent. The code below is a working example (C++11, translates with g++ 4.8.2) with smart pointers and factory function create. Should be quite failsafe. (You can always destroy something in C++ if you really want to.)

#include <memory>
#include <vector>
#include <algorithm>
#include <iostream>
#include <sstream>

class Entity;


typedef std::shared_ptr<Entity> ptr_t;
typedef std::vector<ptr_t> animals_t;

class Entity {
protected:
    Entity() {};
public:
    template<class E, typename... Arglist>
    friend ptr_t create(Arglist... args) {
        return ptr_t(new E(args...));
    }

    animals_t my_friends;

    virtual std::string what()=0;
};

class Cow : public Entity {
    double m_milk;
    Cow(double milk) :
        Entity(),
        m_milk(milk)
    {}
public:
    friend ptr_t create<Cow>(double);

    std::string what() {
        std::ostringstream os;
        os << "I am a cow.\nMy mother gave " << m_milk << " liters.";
        return os.str();
    }
};

class Pig : public Entity {
    Pig() : Entity() {}
public:
    friend ptr_t create<Pig>();

    std::string what() {
        return "I am a pig.";
    }
};

int main() {
    animals_t animals;

    animals.push_back(create<Cow>(1000.0));
    animals.push_back(create<Pig>());

    animals[0]->my_friends.push_back(animals[1]);

    std::for_each(animals.begin(),animals.end(),[](ptr_t v){
            std::cout << '\n' << v->what();
            std::cout << "\nMy friends are:";
            std::for_each(v->my_friends.begin(),v->my_friends.end(),[](ptr_t f){
                    std::cout << '\n' << f->what();
                });
            std::cout << '\n';
        });
}

/*
    Local Variables:
    compile-command: "g++ -std=c++11 test.cc -o a.exe && ./a.exe"
    End:
 */

By declaring the Entity constructor as protected one forces that derived objects of type Entity must be created through the factory function create.

The factory makes sure that objects always to into smart pointers (in our case std::shared_ptr).

It is written as template to make it re-usable in the subclasses. More precisely it is a variadic template to allow constructors with any numbers of arguments in the derived classes. In the modified version of the program one passes the milk production of the mother cow to the newly created cow as a constructor argument.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top