【问题标题】:How can I create a polymorphic object on the stack?如何在堆栈上创建多态对象?
【发布时间】:2012-08-12 00:50:14
【问题描述】:

如何在堆栈上分配多态对象?我正在尝试做类似的事情(试图避免使用 new 进行堆分配)?:

A* a = NULL;

switch (some_var)
{
case 1:
    a = A();
    break;
case 2:
    a = B(); // B is derived from A
    break;
default:
    a = C(); // C is derived from A
    break;
}

【问题讨论】:

  • 除了提到的解决方案之外,您可以尝试使用指令 ifdef 和 define
  • @MoatazElmasry #defines 在运行时绝不是多态的,因此不允许按照问题中的说明进行切换。
  • 为什么要避免堆分配?
  • @user - 如果你想做三个不同的事情,为什么不写三个不同的函数呢? Switch-statement-polymorphism 几乎总是表明你做错了什么。
  • 签出polymorphic_value为c++20提出的建议

标签: c++ stack polymorphism


【解决方案1】:

我为此编写了一个通用模板。完整代码可用here(对于这个问题的范围来说太复杂了)。 StackVariant 对象包含提供的类型中最大类型大小的缓冲区,以及最大对齐方式。 Object 是使用“placement new”在堆栈上构造的,operator->() 用于多态访问以建议间接。此外,重要的是要确保如果定义了虚拟 detor,则应在销毁堆栈上的对象时调用它,因此模板 detor 只是使用 SFINAE 定义来执行此操作。

(参见下面的使用示例和输出):

//  compile: g++ file.cpp -std=c++11
#include <type_traits>
#include <cstddef>

// union_size()/union_align() implementation in gist link above

template<class Tbaseclass, typename...classes>
class StackVariant {
    alignas(union_align<classes...>()) char storage[union_size<classes...>()];
public:
    inline Tbaseclass* operator->() { return ((Tbaseclass*)storage); }
    template<class C, typename...TCtor_params>
    StackVariant& init(TCtor_params&&...fargs)
    {
        new (storage) C(std::forward<TCtor_params>(fargs)...);      // "placement new"
        return *this;
    };


    template<class X=Tbaseclass>
    typename std::enable_if<std::has_virtual_destructor<X>::value, void>::type
    call_dtor(){
        ((X*)storage)->~X();
    }

    template<class X=Tbaseclass>
    typename std::enable_if<!std::has_virtual_destructor<X>::value, void>::type
    call_dtor() {};

    ~StackVariant() {
        call_dtor();
    }
};

使用示例:

#include <cstring>
#include <iostream>
#include "StackVariant.h"

class Animal{
public:
    virtual void makeSound() = 0;
    virtual std::string name() = 0;
    virtual ~Animal() = default;
};

class Dog : public Animal{
public:
    void makeSound() final { std::cout << "woff" << std::endl; };
    std::string name() final { return "dog"; };
    Dog(){};
    ~Dog() {std::cout << "woff bye!" << std::endl;}
};

class Cat : public Animal{
    std::string catname;
public:
    Cat() : catname("gonzo") {};
    Cat(const std::string& _name) : catname(_name) {};
    void makeSound() final { std::cout << "meow" << std::endl; };
    std::string name() final { return catname; };
};

using StackAnimal = StackVariant<Animal, Dog, Cat>;

int main() {
    StackAnimal a1;
    StackAnimal a2;
    a1.init<Cat>("gonzo2");
    a2.init<Dog>();  
    a1->makeSound();
    a2->makeSound();
    return 0;
}
//  Output:
//  meow
//  woff
//  woff bye!

注意事项:

  1. 我在编写它时试图避免在性能关键函数中进行堆分配,它确实做到了 - 速度提高了 50%。
  2. 我编写它是为了利用 C++ 自己的多态机制。在此之前,我的代码中充满了 switch-case,就像之前的建议一样。

【讨论】:

  • 不错的解决方案 (+1) 我可以建议使用 perfect forwarding 作为构造函数参数
【解决方案2】:

运行这个简短的程序,你就会明白为什么多态对象不能很好地在堆栈上工作。当您创建一个未知派生类型的堆栈对象并期望它从函数调用中返回时,当调用函数超出范围时,会发生该对象被销毁的情况。因此,该对象仅在该功能在范围内时才存在。为了返回一个比调用函数寿命更长的有效对象,您需要使用堆。这通过这个简单的层次结构和具有 switch 语句的相同函数的两个版本来演示,除了一个在堆栈上执行,另一个在堆上执行。查看两个实现的输出并查看调用了哪些方法、从哪个类调用它们以及调用它们的时间。

#include <string>
#include <iostream>

class Base {
public:
    enum Type {
        DERIVED_A = 0,
        DERIVED_B,
        DERIVED_C
    };

protected:
    Type type_;

public:
    explicit Base(Type type) : type_(type) {
        std::cout << "Base Constructor Called." << std::endl;
    }
    virtual ~Base() {
        std::cout << "Base Destructor Called." << std::endl;
    }

    virtual void doSomething() {
        std::cout << "This should be overridden by derived class without making this a purely virtual method." << std::endl;
    }

    Type getType() const { return type_; }
};

class DerivedA : public Base {
public:
    DerivedA() : Base(DERIVED_A) {
        std::cout << "DerivedA Constructor Called." << std::endl;
    }
    virtual ~DerivedA() {
        std::cout << "DerivedA Destructor Called." << std::endl;
    }

    void doSomething() override {
        std::cout << "DerivedA overridden this function." << std::endl;
    }
};

class DerivedB : public Base {
public:
    DerivedB() : Base(DERIVED_B) {
        std::cout << "DerivedB Constructor Called." << std::endl;
    }
    virtual ~DerivedB() {
        std::cout << "DerivedB Destructor Called." << std::endl;
    }

    void doSomething() override {
        std::cout << "DerivedB overridden this function." << std::endl;
    }
};

class DerivedC : public Base {
public:
    DerivedC() : Base(DERIVED_C) {
        std::cout << "DerivedC Constructor Called." << std::endl;
    }
    virtual ~DerivedC() {
        std::cout << "DerivedC Destructor Called." << std::endl;
    }

    void doSomething() override {
        std::cout << "DerivedC overridden this function." << std::endl;
    }
};    

Base* someFuncOnStack(Base::Type type) {
    Base* pBase = nullptr;

    switch (type) {
        case Base::DERIVED_A: {
            DerivedA a;
            pBase = dynamic_cast<Base*>(&a);
            break;
        }
        case Base::DERIVED_B: {
            DerivedB b;
            pBase = dynamic_cast<Base*>(&b);
            break;
        }
        case Base::DERIVED_C: {
            DerivedC c;
            pBase = dynamic_cast<Base*>(&c);
            break;
        }
        default: {
            pBase = nullptr;
            break;
        }
    }
    return pBase;
}

Base* someFuncOnHeap(Base::Type type) {
    Base* pBase = nullptr;

    switch (type) {
        case Base::DERIVED_A: {
        DerivedA* pA = new DerivedA();
        pBase = dynamic_cast<Base*>(pA);
        break;
        }
        case Base::DERIVED_B: {
        DerivedB* pB = new DerivedB();
        pBase = dynamic_cast<Base*>(pB);
        break;
        }
        case Base::DERIVED_C: {
        DerivedC* pC = new DerivedC();
        pBase = dynamic_cast<Base*>(pC);
        break;
        }
        default: {
        pBase = nullptr;
        break;
        }
    }
    return pBase;    
}

int main() {

    // Function With Stack Behavior
    std::cout << "Stack Version:\n";
    Base* pBase = nullptr;
    pBase = someFuncOnStack(Base::DERIVED_B);
    // Since the above function went out of scope the classes are on the stack
    pBase->doSomething(); // Still Calls Base Class's doSomething
    // If you need these classes to outlive the function from which they are in
    // you will need to use heap allocation.

    // Reset Base*
    pBase = nullptr;

    // Function With Heap Behavior
    std::cout << "\nHeap Version:\n";
    pBase = someFuncOnHeap(Base::DERIVED_C);
    pBase->doSomething();

    // Don't Forget to Delete this pointer
    delete pBase;
    pBase = nullptr;        

    char c;
    std::cout << "\nPress any key to quit.\n";
    std::cin >> c;
    return 0;
}

输出:

Stack Version:
Base Constructor Called.
DerivedB Constructor Called.
DerivedB Destructor Called.
Base Destructor Called.
This should be overridden by derived class without making this a purely virtual method.

Heap Version:
Base Constructor Called.
DerivedC Constructor Called.
DerivedC overridden this function.
DerivedC Destructor called.
Base Destructor Called. 

我不是说不能做;我只是在说明尝试这样做的注意事项。尝试做类似的事情可能是不明智的。我不知道有什么方法可以做到这一点,除非你有一个包装类,它包含堆栈分配的对象来管理它们的生命周期。我将不得不尝试并努力解决这个问题,看看我是否能想出类似的东西。

【讨论】:

    【解决方案3】:

    免责声明:我绝对不认为这是一个好的解决方案。好的解决方案是重新考虑设计(鉴于存在有限数量的可能性,这里可能不保证 OO 多态性?),或者使用第二个函数通过引用传递所述多态对象。

    但由于其他人提到了这个想法,但细节错误,我发布这个答案是为了展示如何正确处理。希望我做对了。

    很明显,可能类型的数量是有限的。这意味着像 boost::variant 这样的有区别的联合可以解决问题,即使它并不漂亮:

    boost::variant<A, B, C> thingy = 
        some_var == 1? static_cast<A&&>(A())
        : some_var == 2? static_cast<A&&>(B())
        : static_cast<A&&>(C());
    

    如果我一直认为这不是对 OO 多态性的良好使用,那么现在您可以使用静态访问者之类的东西就是其中之一。

    如果您想按照其他答案中的建议手动使用新放置而不是现成的解决方案,则需要注意许多事情,因为我们在此过程中失去了常规自动对象的一些属性:

    • 编译器不再为我们提供正确的大小和对齐方式;
    • 我们不再自动调用析构函数;

    在 C++11 中,这些都可以分别使用 aligned_unionunique_ptr 轻松修复。

    std::aligned_union<A, B, C>::type thingy;
    A* ptr;
    switch (some_var)
    {
    case 1:
        ptr = ::new(&thingy.a) A();
        break;
    case 2:
        ptr = ::new(&thingy.b) B();
        break;
    default:
        ptr = ::new(&thingy.c) C();
        break;
    }
    std::unique_ptr<A, void(*)(A*)> guard { ptr, [](A* a) { a->~A(); } };
    // all this mechanism is a great candidate for encapsulation in a class of its own
    // but boost::variant already exists, so...
    

    对于不支持这些特性的编译器,你可以得到替代方案:Boost 包括 aligned_storagealignment_of 特征,可用于构建 aligned_union;并且unique_ptr 可以替换为某种范围保护类。

    现在已经不碍事了,很清楚,不要这样做,只是将临时传递给另一个函数,或者重新审视设计。

    【讨论】:

      【解决方案4】:

      不能创建多态局部变量

      您不能创建多态局部变量,因为A 的子类B 可能具有比A 更多的属性,因此占用更多空间,因此编译器必须为最大的子类保留足够的空间A.

      1. 如果你有几十个子类,其中一个有大量属性,这会浪费很多空间。
      2. 如果您将接收到的A 子类的实例作为参数放入局部变量中,并且您将代码放入动态库中,那么与其链接的代码可以声明一个大于你的库,所以编译器无论如何都不会在堆栈上分配足够的空间。

      所以自己给它分配空间

      使用placement new,您可以通过其他方式在您分配的空间中初始化对象:

      但是,这些技术可能会占用大量额外空间,并且如果您获得的引用(指针)指向 A 的未知编译时子类,且该子类大于您所使用的类型,则这些技术可能无法使用占了。

      我建议的解决方案是在每个子类上都有一种工厂方法,它调用一个提供的函数,该函数带有一个指向给定子类的堆栈分配实例的指针。我在提供的函数的签名中添加了一个额外的 void* 参数,因此可以将任意数据传递给它。

      @MooingDuck 在下面的评论中建议 this implementation 使用模板和 C++11。如果您需要它用于无法从 C++11 功能中受益的代码,或者对于一些带有结构而不是类的普通 C 代码(如果 struct B 具有 struct A 类型的第一个字段,则可以对其进行操作有点像A 的“子结构”),那么我下面的版本就可以解决问题(但不是类型安全的)。

      这个版本适用于新定义的子类,只要它们实现ugly类工厂方法,并且它将使用一个恒定数量的堆栈用于返回地址和该中间函数所需的其他信息,加上大小所请求类的实例的大小,但不是最大子类的大小(除非您选择使用那个)。

      #include <iostream>
      class A {
          public:
          int fieldA;
          static void* ugly(void* (*f)(A*, void*), void* param) {
              A instance;
              return f(&instance, param);
          }
          // ...
      };
      class B : public A {
          public:
          int fieldB;
          static void* ugly(void* (*f)(A*, void*), void* param) {
              B instance;
              return f(&instance, param);
          }
          // ...
      };
      class C : public B {
          public:
          int fieldC;
          static void* ugly(void* (*f)(A*, void*), void* param) {
              C instance;
              return f(&instance, param);
          }
          // ...
      };
      void* doWork(A* abc, void* param) {
          abc->fieldA = (int)param;
          if ((int)param == 4) {
              ((C*)abc)->fieldC++;
          }
          return (void*)abc->fieldA;
      }
      void* otherWork(A* abc, void* param) {
          // Do something with abc
          return (void*)(((int)param)/2);
      }
      int main() {
          std::cout << (int)A::ugly(doWork, (void*)3);
          std::cout << (int)B::ugly(doWork, (void*)1);
          std::cout << (int)C::ugly(doWork, (void*)4);
          std::cout << (int)A::ugly(otherWork, (void*)2);
          std::cout << (int)C::ugly(otherWork, (void*)11);
          std::cout << (int)B::ugly(otherWork, (void*)19);
          std::cout << std::endl;
          return 0;
      }
      

      到那时,我认为我们可能已经超过了简单的malloc 的成本,所以你可能想使用它。

      【讨论】:

      • 您实际上可以在堆栈上创建一个多态类型——请参阅演示放置新的答案。它被语言很好地定义/支持。
      • 这个论点遗漏了一个细节:临时文件工作正常,编译器完全能够决定堆栈需要多大的大小。
      • @Justin:我澄清了我的回答,我的意思是如果没有手动分配,你不能用“经典”变量来做到这一点。
      • @R.MartinhoFernandes :确实,临时变量可以正常工作,但变量不是临时变量,它会在函数的整个生命周期内持续存在。如果你有两个这样的动态变量,并且在开始时都持有一个小实例,然后你把一个在另一个库中创建的子类的大实例放在另一个库中:编译器怎么知道它应该为第一个变量保留足够的空间,所以它不会不会撞到第二个?它需要在链接时移动变量在堆栈上的位置,我敢打赌它不会。临时变量之所以有效,是因为我们在编译时就知道它们的具体类型。
      • 你的代码看起来像this的一个非常丑陋且不安全的版本
      【解决方案5】:

      您可以使用新展示位置来做到这一点。这会将项目放在堆栈上,在缓冲区中包含的内存中。但是,这些变量不是自动的。缺点是你的析构函数不会自动运行,当它们超出范围时,你需要像创建它们一样正确地销毁它们。

      手动调用析构函数的合理替代方法是将类型包装在智能指针中,如下所示:

      class A
      {
      public:
         virtual ~A() {}
      };
      
      class B : public A {};
      class C : public B {};
      
      template<class T>
      class JustDestruct
      {
      public:
         void operator()(const T* a)
         {
            a->T::~T();
         }
      };
      
      void create(int x)
      {
          char buff[1024] // ensure that this is large enough to hold your "biggest" object
          std::unique_ptr<A, JustDestruct<T>> t(buff);
      
          switch(x)
          {
          case 0:
             ptr = new (buff) A();
             break;
      
          case 1:
             ptr = new (buff) B();
             break;
      
          case 2:
             ptr = new (buff) C();
             break;
          }
      
          // do polymorphic stuff
      }
      

      【讨论】:

      • 当然,在实践中应该使用 RAII,以便自动调用析构函数。我相信带有自定义删除器的std::unique_ptr 可以工作。
      • 那将是首选,是的。但是,在 OP 建议的情况下(他想避免堆分配),可能没有太多需要清理的地方。
      • @Chad:创建一个只调用析构函数的unique_ptr 很容易。
      • @Chad:呃,为什么还要关心这个案子?只需第一次做对,然后您就不必担心了。
      • 这段代码完全忽略了这样一个事实,即不能保证数组对于所有涉及的类型都会正确对齐。
      【解决方案6】:

      如果 B 是您的基本类型 D1、D2 和 D3 是您的派生类型:

      void foo()
      {
          D1  derived_object1;
          D2  derived_object2;
          D3  derived_object3;
          B *base_pointer;
      
          switch (some_var)
          {
              case 1:  base_pointer = &derived_object1;  break;
              ....
          }
      }
      

      如果你想避免浪费三个派生对象的空间,你可以把你的方法分成两部分;选择您需要的类型的部分,以及适用于它的方法的部分。在决定了你需要哪种类型之后,你调用一个分配该对象的方法,创建一个指向它的指针,然后调用该方法的后半部分来完成堆栈分配对象的工作。

      【讨论】:

        【解决方案7】:

        可能的,但要干净利落地做很多工作(也就是说,无需手动放置新的和暴露的原始缓冲区)。

        您正在查看类似Boost.Variant 的内容,经过修改以将类型限制为基类和一些派生类,并且以公开对基类型的多态引用。

        这个东西 (PolymorphicVariant ?) 会为你包装所有放置的新东西(并且还负责安全销毁)。

        如果这真的是你想要的,请告诉我,我会给你一个开始。除非您真的完全需要这种行为,否则 Mike Seymour 的建议更实用。

        【讨论】:

          【解决方案8】:

          你不能构造一个函数来像那样工作,因为在条件块内创建的自动或临时对象不能将它们的生命周期延长到包含块中。

          我建议将多态行为重构为一个单独的函数:

          void do_something(A&&);
          
          switch (some_var)
          {
          case 1:
              do_something(A());
              break;
          case 2:
              do_something(B()); // B is derived from A
              break;
          default:
              do_something(C()); // C is derived from A
              break;
          }
          

          【讨论】:

            【解决方案9】:

            试图避免使用 new) 进行堆分配?

            那么在这种情况下,您像往常一样在堆栈上创建对象并将地址分配给基指针。但是请记住,如果这是在函数内部完成的,请不要将地址作为返回值传递,因为函数调用返回后堆栈会展开。

            所以这很糟糕。

            A* SomeMethod()
            {
                B b;
                A* a = &b; // B inherits from A
                return a;
            }
            

            【讨论】:

            • 是的,我知道。这是“仅供参考”... :)
            【解决方案10】:

            严格回答您的问题 - 您现在所拥有的就是这样做的 - 即 a = A();a = B()a = C(),但这些对象是切片的。

            为了实现多态行为使用您拥有的代码,我担心这是不可能的。编译器需要事先知道对象的大小。除非你有引用或指针。

            如果您使用 指针,您需要确保它不会最终悬空:

            A* a = NULL;
            
            switch (some_var)
            {
            case 1:
                A obj;
                a = &obj;
                break;
            }
            

            因为obj 超出范围而无法工作。所以你只剩下:

            A* a = NULL;
            A obj1;
            B obj2;
            C obj3;
            switch (some_var)
            {
            case 1:
                a = &obj1;
                break;
            case 2:
                a = &obj2;
                break;
            case 3:
                a = &obj3;
                break;
            }
            

            这当然是浪费。

            对于引用,这有点棘手,因为它们必须在创建时分配,并且您不能使用临时对象(除非它是 const 引用)。因此,您可能需要一个返回持久引用的工厂。

            【讨论】:

              【解决方案11】:

              char 数组和放置 new 的组合将起作用。

              char buf[<size big enough to hold largest derived type>];
              A *a = NULL;
              
              switch (some_var)
              {
              case 1:
                  a = new(buf) A;
                  break;
              case 2:
                  a = new(buf) B;
                  break;
              default:
                  a = new(buf) C;
                  break;
              }
              
              // do stuff with a
              
              a->~A(); // must call destructor explicitly
              

              【讨论】:

              • 我会改用aligned_union&lt;A, B, C&gt;::type 来保证正确的大小和正确的对齐方式。
              • @R.MartinhoFernandes ......但是这个问题缺少一个清晰、干净、赞成的答案来证明它是如何工作的。你删除了你的(这 hacky但至少有效)。你为什么不用aligned_union写一个新的?
              【解决方案12】:

              多态性不适用于值,您需要引用或指针。您可以多态地使用对临时对象的 const 引用,它将具有堆栈对象的生命周期。

              const A& = (use_b ? B() : A());
              

              如果您需要修改对象,您别无选择,只能动态分配它(除非您使用 Microsoft 的非标准扩展,该扩展允许您将临时对象绑定到非常量引用)。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2023-03-18
                • 1970-01-01
                • 2010-09-24
                • 2010-12-08
                • 1970-01-01
                • 1970-01-01
                • 2016-12-15
                • 2020-04-09
                相关资源
                最近更新 更多