如何在堆栈上创建多态对象?

时间:2012-08-15 17:32:17

标签: c++ stack polymorphism

如何在堆栈上分配多态对象?我正在尝试做类似的事情(试图避免使用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;
}

12 个答案:

答案 0 :(得分:7)

免责声明:我绝对不认为这是一个很好的解决方案。好的解决方案是重新考虑设计(假设存在有限数量的可能性,可能不保证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_storage的{​​{1}}和alignment_of个特征;并且aligned_union可以替换为某种范围内防守等级。

现在已经不在了,只是如此清楚,不要这样做,只是简单地传递给另一个功能,或者完全重新考虑设计。

答案 1 :(得分:7)

您不能将单个函数构造成这样的工作,因为在条件块内创建的自动或临时对象的生命周期不能扩展到包含块中。

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

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

答案 2 :(得分:4)

如果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;
        ....
    }
}

如果你想避免浪费三个派生对象的空间,你可以将你的方法分成两部分;选择所需类型的部分,以及对其起作用的方法部分。确定了所需的类型后,调用一个方法来分配该对象,创建一个指向它的指针,并调用方法的后半部分来完成堆栈分配对象的工作。

答案 3 :(得分:3)

我写了一个通用模板来做到这一点。可用的完整代码here(这个问题的范围变得过于复杂)。 StackVariant对象包含一个提供类型中最大类型的缓冲区,以及最大的对齐方式。 Object使用'placement new'构建在堆栈上,operator-&gt;()用于多态访问以建议间接。此外,重要的是要确保如果定义了虚拟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 ++自己的多态机制。在此之前,我的代码充满了像之前的建议一样的开关案例。

答案 4 :(得分:2)

您无法创建多态局部变量

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

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

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

    但是,这些技术可能会占用大量额外空间,如果给定的A未知编译时子类的引用(指针)大于你占的类型。

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

    @MooingDuck在下面的评论中使用模板和C ++ 11建议this implementation。如果你需要这个代码无法从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的成本,所以你可能会想要使用它。

答案 5 :(得分:1)

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

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

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
}

答案 6 :(得分:0)

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

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

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

答案 7 :(得分:0)

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

答案 8 :(得分:0)

要严格回答您的问题 - 您现在拥有的内容 - 即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引用)。所以你可能需要一个返回持久引用的工厂。

答案 9 :(得分:0)

  

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

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

所以这很糟糕。

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

答案 10 :(得分:0)

是可能的,但是干净利落的努力是很多(没有手动放置新的和暴露的原始缓冲区,就是这样)。

您正在查看类似Boost.Variant的内容,修改为将类型限制为基类,并使用某些派生类来公开对基类型的多态引用。

这个东西( PolymorphicVariant ?)会为你包装所有放置的新东西(并且还要处理安全破坏)。

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

答案 11 :(得分:0)

运行这个简短的程序,你就会明白为什么多态对象不能很好地在堆栈上运行。当您创建一个未知的派生类型的堆栈对象并期望它从函数调用返回时,会发生什么是当该调用函数超出范围时该对象被销毁。因此,只要该函数在范围内,该对象就会存在。为了返回一个比调用函数更长的有效对象,您需要使用堆。这是通过这个简单的层次结构和带有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. 

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