scirocc
scirocc

Reputation: 183

How can I maintain polymorphism when creating an instance on stack in C++?

To create instance on the heap and maintain polymorphism, and that'll give the right answer:

class Father
{
    public:

        virtual void Say()
        {
            cout << "Father say hello" << endl;
        }
};


class Son : public Father
{
    public:
        void Say()
        {
            cout << "Son say hello" << endl;
        }
};

int main()
{
    std::vector<Father*> v;
    std::cout << 1 << std::endl;

    for(int i(0); i<5; i++)
    {
        auto p = new Son();    ---------------on heap
        v.emplace_back(p);
    }
    for(auto p : v)
    {
        p->Say();
    }
}

But when I want to create an instance on the stack, it seems not so easy:

Edition 1:

class Father
{
    public:

        virtual void Say()
        {
            cout << "Father say hello" << endl;
        }
};


class Son : public Father
{
    public:
        void Say()
        {
            cout << "Son say hello" << endl;
        }
};


int main()
{
    std::vector<Father> v;
    for(int i(0); i<5; i++)
    {
        auto o = Son();    ---------------on stack
        v.emplace_back(o);---------------now "o" is cast to Father type
    }

    for(auto o : v)
    {
        o.Say();------------------------only output "Father say hello"
    }
}

And edition 2:

class Father
{
    public:

        virtual void Say()
        {
            cout << "Father say hello" << endl;
        }
    };


class Son : public Father
{
    public:
        void Say()
        {
            cout << "Son say hello" << endl;
        }
};


int main()
{
    std::vector<Father*> v;
    for(int i(0); i<5; i++)
    {
        auto p = &Son();    --------------On the stack
        v.emplace_back(p);---------------Now "o" is cast to Father type
    }

    for(auto p : v)
    {
        p->Say();------------------------Since "p" now is a Wild pointer, it'll fail too
    }
}

Can this be fixed? Or is it just a dead end: If I want to use polymorphism, then I have to create an object on the heap.

Upvotes: 2

Views: 168

Answers (4)

The question doesn't have much to do with stack. You're really asking how to implement polymorphism when storing by value. It's not too hard if you can use C++17 and thus have std::variant available.

The implementation is surprisingly simple:

#include <algorithm>
#include <cassert>
#include <variant>
#include <vector>

enum class Who { Father, Son };

struct ISayer {
    virtual Who Me() const = 0;
    virtual ~ISayer() {};
};

struct Father final : ISayer {
    Who Me() const override { return Who::Father; }
};

struct Son final : ISayer {
    Who Me() const override { return Who::Son; }
};

struct AnySayer0 : std::variant<Father, Son>
{
    using variant_type = std::variant<Father, Son>;
    using variant_type::variant;
    operator const ISayer &() const {
        return std::visit([](auto &val) -> const ISayer &{ return val; },
        static_cast<const variant_type &>(*this));
    }
    operator ISayer &() {
        return std::visit([](auto &val) -> ISayer &{ return val; },
        static_cast<variant_type &>(*this));
    }
    const ISayer *operator->() const { return &static_cast<const ISayer &>(*this); }
    ISayer *operator->() { return &static_cast<ISayer &>(*this); }
};

using AnySayer = AnySayer0;

int main()
{
    std::vector<AnySayer> people;

    people.emplace_back(std::in_place_type<Father>);
    people.emplace_back(std::in_place_type<Son>);

    assert(people.front()->Me() == Who::Father);
    assert(people.back()->Me() == Who::Son);
}

An alternate implementation AnySayer1 would need a bit more boilerplate, and perhaps be a bit faster - but would it be a bit smaller as well?

struct AnySayer1
{
    template <typename ...Args>
    AnySayer1(std::in_place_type_t<Father>, Args &&...args) :
        father(std::forward<Args>(args)...), ref(father) {}
    template <typename ...Args>
    AnySayer1(std::in_place_type_t<Son>, Args &&...args) :
        son(std::forward<Args>(args)...), ref(son) {}
    ~AnySayer1() { ref.~ISayer(); }
    operator const ISayer &() const { return ref; }
    operator ISayer &() { return ref; }
    const ISayer *operator->() const { return &static_cast<const ISayer &>(*this); }
    ISayer *operator->() { return &static_cast<ISayer &>(*this); }
    AnySayer1(AnySayer1 &&o) : ref(getMatchingRef(o)) {
        if (dynamic_cast<Father*>(&o.ref))
            new (&father) Father(std::move(o.father));
        else if (dynamic_cast<Son*>(&o.ref))
            new (&son) Son(std::move(o.son));
    }
    AnySayer1(const AnySayer1 &o) : ref(getMatchingRef(o)) {
        if (dynamic_cast<Father*>(&o.ref))
            new (&father) Father(o.father);
        else if (dynamic_cast<Son*>(&o.ref))
            new (&son) Son(o.son);
    }
    AnySayer1 &operator=(const AnySayer1 &) = delete;
private:
    union { 
        Father father;
        Son son;
    };
    ISayer &ref;
    ISayer &getMatchingRef(const AnySayer1 &o) {
        if (dynamic_cast<const Father *>(&o.ref))
            return father;
        if (dynamic_cast<const Son *>(&o.ref))
            return son;
        assert(false);
    }
};

This could be rewritten using the same "magic" that makes std::variant work - it'd be less repetitive that way.

But - is it smaller?

static_assert(sizeof(AnySayer1) == sizeof(AnySayer0));

No. At least in both gcc and clang, both implementations have the same size. And that makes sense, since std::variant doesn't need to store any more information than we do - it only needs to keep some way to discriminate the type. We chose to use the reference to ISayer and use dynamic type information, since that optimizes for the common case when we convert to the interface type - we store the reference ready to use. std::variant can't assume that the types have a common base, and instead stores an integer type index instead and uses generated code to dispatch on that index. It may be generally a bit slower in the path that uses the visitor to return the reference - but not necessarily, since the compiler can note that both types have their ISayer vtable pointer at the same location, and can squash the type-based dispatch down to a "has value vs has no value" test. It seems that the most recent versions of all major C++ compilers (gcc, clang and MSVC) easily deal with this and generate code that is just as fast as our "optimized" AnySayer1.

Upvotes: 0

463035818_is_not_an_ai
463035818_is_not_an_ai

Reputation: 122840

In general polymorphism does not require dynamic allocations. That's a common misunderstanding, and hence here comes a counter example:

void foo(const Father& f) { f.Say(); }

Son s;
foo(s);

You have to declare Say as const to make it work, but then it will print the expected Son say hello. You need references or pointers for polymorphism, not necessarily dynamic allocation!

Having said this, when you want a container of derived classes, then std::vector<Father> won't do. Public inheritance models a "is-a" relation, so a Son is a Father, but a Father is not a Son (notice how wrong and misleading the father-son analogy is?!?). Hence when you put a Son into a vector of Fathers, then the object gets sliced and only the Father part is stored in the vector (read about "object slicing").

Moreover, auto p= &Son(); is wrong, because the created object is temporary and its life time ends at the end of that line. The pointer you store in the vector is dangling (it points to an object whose lifetime already ended).

To store pointers in the container you can use dynamic allocations. For example, with std::unique_ptrs:

int main()
{
    std::vector<std::unique_ptr<Father>> v;
    for(int i(0);i<5;i++){
        v.emplace_back(new Son);
    }
    for(auto& p:v){
        p->Say();
    }

}

Note that you have to use auto& for the range based for loop, because unique_ptr doesn't copy. unique_ptr does the dirty work: the objects will get deleted automatically when the unique_ptrs get destroyed (which is when the vector goes out of scope).

Upvotes: 2

Zlatomir
Zlatomir

Reputation: 7034

There are lots of things you are doing wrong. First here is how you can do it right:

int main()
{
    Father father1;
    Son son1;
    Father father2;
    Son son2;

    std::vector<Father*> v;
    v.emplace_back(&father1);
    v.emplace_back(&son1);
    v.emplace_back(&father2);
    v.emplace_back(&son2);

    for (auto p : v) {
        p->Say();
    }
}

Basically you need to allocate the objects on the stack so that the objects will be available for as long as the vector is.

Now what you did is undefined behavior, because you basically had pointers (in the vector) to objects that were already de-allocated (even if you fix what was already said in the comments).

    for(int i(0);i<5;i++){
        Son s;
        v.emplace_back(&s);
    // s lifetime end here, but the vector still has pointers to objects that are de-allocated
    }

For this bit: v.emplace_back(p);---------------now "o" is cast to Father type.

I think you tried something completely different: a vector of Father objects std::vector<Father> and if you try to add Son elements into that you get object slicing // I just added this bit so that you can look it up, this is not the main point here

Upvotes: 0

Picaud Vincent
Picaud Vincent

Reputation: 10982

This is a recurring problem/dilemma: you can maintain a value semantic at the expense of some boilerplate code. Here is a minimal working example of such kind of idea:

#include <iostream>
#include <memory>
#include <vector>

class Father
{
 protected:
  struct Father_Interface
  {
    virtual void
    Say() const
    {
      std::cout << "Father say hello" << std::endl;
    }
  };

  using pimpl_type = std::shared_ptr<const Father_Interface>;
  pimpl_type _pimpl;

  Father(const Father_Interface* p) : _pimpl(p) {}

 public:
  Father() : Father{new Father_Interface{}} {}

  void Say() const { _pimpl->Say(); }
};

class Son : public Father
{
 protected:
  class Son_Interface : public Father_Interface
  {
    void
    Say() const override
    {
      std::cout << "Son say hello" << std::endl;
    }
  };

 public:
  Son() : Father{new Son_Interface{}} {}

  Son& operator=(const Father&) = delete; // fight against object slicing
};

int
main()
{
  std::vector<Father> v;

  v.emplace_back(Father());
  v.emplace_back(Son());
  v.emplace_back(Father());

  for (const auto& v_i : v)
  {
    v_i.Say();
  }
}

which prints:

Father say hello
Son say hello
Father say hello

You can also read about:

Upvotes: 1

Related Questions