Как создать полиморфный объект в стеке?

как выделить полиморфный объект в стеке? Я пытаюсь сделать что-то подобное (пытаясь избежать выделения кучи с новым)?:

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 ответов


вы не можете структурировать одну функцию для такой работы, так как автоматические или временные объекты, созданные внутри условного блока, не могут продлевать срок службы содержащего блока.

Я бы предложил рефакторинг полиморфного поведения в отдельную функцию:

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

отказ от ответственности: я определенно не думаю, что это хорошее решение. Хорошие решения - либо переосмыслить дизайн (возможно, полиморфизм 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_storage и alignment_of черты, которые можно использовать для построения aligned_union; и unique_ptr можно заменить какой-то объем класса гвардии.

теперь, когда это в стороне, просто чтобы было ясно, не делайте этого и просто передайте временную функцию другой функции или полностью пересмотрите дизайн.


Если 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;
        ....
    }
}

Если вы хотите избежать потери пространства трех производных объектов, вы можете разбить свой метод на две части: часть, которая выбирает, какой тип вам нужен, и часть метода, который работает на нем. Решив, какой тип вам нужен, вы вызываете метод, который выделяет этот объект, создает указатель на него и вызывает вторую половину метода для завершения работы над выделенным стеком объект.


Я написал общий шаблон для этого. Полный код доступен здесь (он стал слишком сложным для сферы охвата этого вопроса). StackVariant объект содержит буфер размером наибольшего типа из предоставленных типов,а также наибольшее выравнивание. Объект строится на стеке с использованием "placement new", а оператор->() используется для полиморфного доступа, чтобы предложить косвенный доступ. Кроме того, важно убедиться, что если виртуальный детор определен, он должен быть вызывается при уничтожении объекта в стеке, поэтому шаблон 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++. До что мой код был полон переключателей, как и предыдущие предложения здесь.

вы не можете создать полиморфную локальную переменную

вы не можете создать полиморфную локальную переменную, так как подкласс B of A может иметь больше атрибутов, чем A, таким образом, занимают больше места, поэтому компилятор должен был бы зарезервировать достаточно места для самого большого подкласса A.

  1. если у вас есть десятки подклассов, и один из них имеет большое количество атрибутов, это будет тратить много места.
  2. в случае, если вы поставили в локальной переменной экземпляр подкласса A вы получили в качестве параметра, и вы помещаете свой код в динамическую библиотеку, тогда код, связывающийся с ним, может объявить подкласс больше, чем в вашей библиотеке, поэтому компилятор все равно не выделил бы достаточно места в стеке.

так выделите место для него сами

используя размещение new, вы можете инициализировать объект в пространстве, которое вы выделили через некоторые другие значит:

  • alloca, но увидев это так вопрос кажется, это не лучший вариант.
  • массив переменной длины, с которым приходит некоторое (не-)удовольствие от переносимости, так как он работает под GCC, но не в стандарте C++ (даже в C++11)
  • aligned_union<A, B, C>::type, как предложил Р. Мартиньо Фернандес в комментарии ответ

однако, эти методы могут использовать много дополнительное пространство и не работают, если вам дана ссылка (указатель) на неизвестный подкласс во время компиляции A это больше, чем типы, которые вы учитывали.

решение, которое я предлагаю, должно иметь своего рода заводской метод для каждого подкласса, который вызывает предоставленную функцию с указателем на экземпляр, выделенный стеком данного подкласса. Я добавил дополнительный параметр void* к сигнатуре поставляемой функции, чтобы можно было передавать произвольные данные.

@MooingDuck предложил реализация использование шаблонов и C++11 в комментариях ниже. Если вам это нужно для кода, который не может извлечь выгоду из функций c++11, или для простого кода C со структурами вместо классов (если struct B имеет первое поле типа struct A, тогда его можно немного манипулировать как "substruct" о 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, так что вы могли бы палочку, чтобы использовать это в конце концов.


вы можете сделать это с помощью размещения new. Это поместит элементы в стек, в память, содержащуюся в буфере. Однако, эти переменные не являются автоматическими. Недостатком является то, что ваши деструкторы не будут запускаться автоматически, вам нужно будет правильно уничтожить их так же, как вы их создали, когда они выходят за рамки.

разумной альтернативой ручному вызову деструктора является перенос типа в смарт-указатель, как показано ниже:

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
}

полиморфизм не работает со значениями, нужна ссылка или указатель. Вы можете использовать const ссылка на временный объект полиморфно и он будет иметь время жизни объекта стека.

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

Если вам нужно изменить объект, у вас нет выбора, кроме как динамически выделить его (если вы не используете нестандартное расширение Microsoft, которое позволяет привязать временный объект к ссылке non-const).


сочетание a 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

чтобы строго ответить на ваш вопрос - что у вас сейчас делает именно это - т. е. 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 ссылка). Поэтому вам, вероятно, понадобится фабрика, которая возвращает постоянную ссылку.


попытка избежать выделения кучи с помощью new)?

Ну в таком случае вы создайте объект в стеке, как обычно, и назначьте адрес базовому указателю. Но помните, что если это сделано внутри функции, не передавайте адрес как возвращаемое значение, потому что стек будет разматываться после возврата вызова функции.

Так это плохо.

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

Это is возможно, но это много усилия сделать чисто (без ручного размещения новых и открытых необработанных буферов, то есть).

вы смотрите на что-то вроде импульс.Вариант, изменено, чтобы ограничить типы базовым классом и некоторыми производными классами, и чтобы предоставить полиморфную ссылку на базовый тип.

Это дело (PolymorphicVariant ?) обернул бы все размещение новых вещей для вы (а также позаботьтесь о безопасном уничтожении).

Если это действительно то, что вы хотите, дайте мне знать, и я дам тебе начать. Если вам действительно не нужно ровно это поведение, хотя предложение Майка Сеймура более практично.


запустите эту короткую программу, и вы увидите, почему полиморфные объекты не работают в стеке очень хорошо. Когда вы создаете объект стека производного типа, который неизвестен, и ожидаете, что он будет возвращен из вызова функции, происходит то, что объект уничтожается, когда вызывающая функция выходит за рамки. Таким образом, объект живет только до тех пор, пока эта функция находится в пределах области. Чтобы вернуть допустимый объект, который переживет вызывающую функцию, вам нужно использовать кучу. Это показали с этой простой иерархией и двумя версиями одной и той же функции с оператором 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. 

Я не говорю, что это невозможно сделать; я просто заявляю предостережения в попытке сделать это. Это может быть неразумно пытаться сделать что-то в этом роде. Я не знаю, как это сделать, если у вас нет класса-оболочки, который будет содержать объекты, выделенные стеком для управления их временем жизни. Я должен постараться и поработать над этим, чтобы увидеть, могу ли я придумать что-то подобное.