8

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

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

12 回答 12

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_union和轻松修复unique_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可以用某种范围保护类替换。

既然这已经不碍事了,那就清楚了,不要这样做,只是简单地将一个临时的传递给另一个函数,或者完全重新审视设计。

于 2012-08-15T17:39:52.893 回答
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;
}
于 2012-08-15T17:47:16.457 回答
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;
        ....
    }
}

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

于 2012-08-15T17:35:11.357 回答
3

我写了一个通用模板来做到这一点。此处提供完整代码(对于此问题的范围而言,它变得过于复杂)。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 ,就像这里之前的建议一样。
于 2017-06-13T22:06:14.453 回答
2

您不能创建多态局部变量

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

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

所以自己分配空间

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

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

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

@MooingDuck在下面的评论中建议使用模板和 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,所以你可能想使用它。

于 2012-08-15T17:34:28.740 回答
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
}
于 2012-08-15T17:40:12.270 回答
0

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

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

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

于 2012-08-15T17:37:48.563 回答
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
于 2012-08-15T17:40:28.980 回答
0

严格回答您的问题-您现在所拥有的就是这样做-即a = A();anda = 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引用)。因此,您可能需要一个返回持久引用的工厂。

于 2012-08-15T17:40:29.960 回答
0

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

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

所以这很糟糕。

A* SomeMethod()
{
    B b;
    A* a = &b; // B inherits from A
    return a;
}
于 2012-08-15T17:42:02.080 回答
0

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

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

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

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

于 2012-08-15T17:50:40.390 回答
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. 

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

于 2017-06-18T08:49:55.947 回答