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

问题描述 投票:2回答:4

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

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

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

c++ polymorphism
4个回答
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
您还可以阅读以下内容:


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

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部分将存储在向量中(请参阅“对象切片”)。

此外,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被销毁时(即向量超出范围时),对象将被自动删除。    

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

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

我认为您尝试了完全不同的方法:父亲对象std::vector<Father>的向量,并且如果尝试在其中添加Son元素,则会得到

对象切片 //我只是添加了一点以便您可以查找,这不是这里的重点


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一样快的代码。        
© www.soinside.com 2019 - 2024. All rights reserved.