Search code examples
c++pointersrtticlass-variablesobject-slicing

Avoiding object-slicing in vector<shared_ptr<Base>>


I'm storing my game's states (collections of entities, essentially) in a vector of shared pointers. When adding states to the vector, the derived part of the states is lost and they revert to the base state class. It all compiles fine, but when I query the states' names, they all come back as DEFAULT_STATE_NAME. I've read plenty of information about object splitting, but I cannot see what is going wrong here.

State.hpp

class State {

protected:

    Game &game;

public:

    typedef shared_ptr<State> Pointer;

    static const StateName name = DEFAULT_STATE_NAME;

    explicit State(Game &game_) : game(game_) ;

    virtual ~State() {}

};

Example derived state class

namespace {

class Overworld : public State {

public:

    static const StateName name;

    Overworld(Game &game) : State(game) {}

};

const StateName Overworld::name = OVERWORLD;

}

Game.hpp

class Game {

private:

    vector<State::Pointer> states;

public:

    void addState(const State::Pointer &state) {
        if(!state)
            throw "invalid state error";

        states.push_back(state);
    }

    // ...

}

Solution

  • In order to access member methods of a derived class through a pointer (or reference) to its base class, you must use polymorphism (which you didn't). For example

    struct Base {
        virtual string name() const { return "Base"; }
    };
    
    struct Derived : Base {
        string name() const override { return "Derived"; }
    };
    
    const Base*ptr = new Derived;
    assert(ptr->name()=="Derived");
    

    Such polymorphism only works with non-static member methods, not with data members nor with static member functions. In your case, there is no polymorphism and hence Base::name remains, well, Base::name.

    In your particular case, there are two other possible solutions, though. First, you can use RTTI, though this is generally frowned upon. Another option is to keep the name as a data member in Base and pass it in at construction:

    struct Base {
        const string name = "Base";
        Base() = default;
      protected:
        Base(string const&n)
        : name(n) {}
    };
    
    struct Derived : Base {
        Derived()
        : Base("Derived") {}
    };
    
    const Base*ptr = new Derived;
    assert(ptr->name=="Derived");
    

    when there is no polymorphism (and hence no virtual table and additional indirection) involved, but at the cost of a data member name.