在C ++中的堆栈上创建实例时如何保持多态性?

时间:2020-06-12 08:41:43

标签: c++ polymorphism

要在堆上创建实例并保持多态,这将给出正确的答案:

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();
    }
}

但是当我想在堆栈上创建一个实例时,这似乎并不容易:

版本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"
    }
}

第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
    }
}

这可以解决吗?还是仅仅是一个死胡同:如果我想使用多态性,那么我必须在堆上创建一个对象。

4 个答案:

答案 0 :(得分:2)

通常,多态不需要动态分配。这是一个常见的误解,因此这里有一个反例:

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

Son s;
foo(s);

您必须将Say声明为const才能使其正常工作,但是它将打印预期的Son say hello。您需要用于多态的引用或指针,而不必动态分配!

话虽如此,当您想要一个派生类的容器时,std::vector<Father>不会这样做。公共继承对“ is-a”关系进行建模,因此SonFather,但是Father不是Son(请注意,父亲-儿子比喻是?!?)。因此,当您将Son放入Father s的向量中时,对象将被切片,并且仅Father部分将被存储在向量中(有关“对象切片”的信息)。 / p>

此外,auto p= &Son();是错误的,因为创建的对象是临时对象,并且其生存期在该行的结尾处结束。您存储在向量中的指针是悬空的(它指向寿命已结束的对象)。

要将指针存储在容器中,可以使用动态分配。例如,使用std::unique_ptr s:

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();
    }

}

请注意,您必须对基于循环的范围使用auto&,因为unique_ptr不会被复制。 unique_ptr做的工作很脏:unique_ptr被销毁时(即向量超出范围时),对象将被自动删除。

答案 1 :(得分:1)

这是一个反复出现的问题/难题:您可以牺牲一些样板代码来维护值语义。这是这种想法的一个最小的可行示例:

#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();
  }
}

打印:

Father say hello
Son say hello
Father say hello

您还可以阅读以下内容:

答案 2 :(得分:0)

您做错了很多事情。首先,这是您如何正确执行操作:

myData()[...

基本上,您需要在堆栈上分配对象,以便这些对象只要向量就可用。

现在您所做的是未定义的行为,因为基本上(在向量中)有一个指针已指向已取消分配的对象(即使您修复了注释中已经说过的内容)。

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();
    }
}

为此: 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 }

我认为您尝试了完全不同的方法:父亲对象v.emplace_back(p);---------------now "o" is cast to Father type的向量,并且如果尝试向其中添加Son元素,则会得到 object slicing // //我只是添加了一点您可以查找它,这不是这里的重点

答案 3 :(得分:0)

这个问题与堆栈无关。您实际上是在问按值存储时如何实现多态。如果您可以使用C ++ 17并因此提供std::variant,这并不是很难。

实现非常简单:

#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);
}

另一种实现方式AnySayer1需要更多样板,也许会更快一些-但它也会更小吗?

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);
    }
};

可以使用使std::variant工作的相同“魔术”来重写它-这样可以减少重复。

但是-它较小吗?

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

不。至少在gcc和clang中,两个实现的大小都相同。这很有意义,因为std::variant不需要存储比我们更多的信息-它只需要保持某种方式来区分类型即可。我们选择使用对ISayer的引用并使用动态类型信息,因为在转换为接口类型时,这种情况针对常见情况进行了优化-我们存储了可供使用的引用。 std::variant不能假定类型具有相同的基数,而是存储整数类型索引,并使用生成的代码在该索引上分派。使用访问者返回引用的路径通常会比较慢-但不是必须的,因为编译器可以注意到两种类型的ISayer vtable指针都位于同一位置,并且可以压缩该类型到“有价值与无价值”的测试。似乎所有主要C ++编译器(gcc,clang和MSVC)的最新版本都可以轻松地处理此问题,并生成与我们的“优化” AnySayer1一样快的代码。