2015-03-03 2 views
5

Мне было интересно, существует ли элегантное решение этой проблемы.Полиморфизм в контейнерах ST ++ STL

Предположим следующее:

class Base { 
    private: 
    int data; 
    protected: 
    Base(int data) : data(data) { } 
    int getData() const { return data; } 
    virtual bool someOp() = 0; 
} 

class Derived1 : public Base { 
    private: 
    int data2; 
    public: 
    Derived1(int data, int data2) : Base(data), data2(data2) { } 
    int getData2() const { return data2; } 
    bool someOp() override { /* something */ } 
} 

class Derived2 : public Base { 
    private: 
    float data2; 
    public: 
    Derived1(int data, float data2) : Base(data), data2(data2) { } 
    float getData2() const { return data2; } 
    bool someOp() override { /* something */ } 
} 

И предположим, что у меня есть полный контроль над иерархией, поэтому я могу предположить, что Base не будут расширены, ни DerivedX класса.

Теперь я хочу сохранить их в std::vector, если я хочу использовать полиморфизм, я вынужден хранить указатели, иначе наложение объектов не позволит мне хранить дополнительные производные свойства. Поэтому я в основном вынужден использовать std::vector<unique_ptr<Base>>.

Но давайте предположим, что мне нужно хранить большое количество этих объектов, и я не хочу тратить для двойного выделения кучи (внутренний std::vector + сам объект), и что в то же самое время, я могу предположить, что:

  • иерархия классов прекрасно определяется и не будет продлен, не зная, что
  • sizeof(DerivedX) не будет таким большим, чем sizeof(Base)

так мне интересно, если есть элегантный способ сохранять полиморфизм и избегать хранения указателей. Я мог бы подумать о некоторых решениях, таких как

class Base { 
    enum { 
    DERIVED1, 
    DERIVED2 
    } type; 

    int data; 
    union { 
    int derived1data; 
    float derived2data; 
    } 

    bool someOp() { 
    if (this->type == DERIVED1) ... 
    else if .. 
    } 
} 

Но это явно не изящно. Я мог бы также попытаться использовать тот факт, что разбиение объектов не должно происходить, если sizeof(Derived) == sizeof(Base) с использованием защищенного объединения в Base и доступа к нему от Derived и литья адреса в элементы в std::vector до нужного типа (в соответствии с перечислением), но это Звучит некрасиво.

+2

Вы можете взглянуть на буст :: вариант ... – Deduplicator

+0

Да, 'повышение :: variant', а связанный с ним' apply_visitor' что пришло на ум для меня. Однако вам нужно знать все производные типы заранее. –

+0

Связь между базой и DerivedX не имеет значения при использовании boost_variant. Также может быть N несвязанных классов. –

ответ

4

Тип стирания и оптимизация небольшого буфера.

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

boost::any тип стирается, чтобы копировать, уничтожать, получать тип и тип обратной привязки к точному соответствию. std::function тип стирает до копирования, вызывается с определенной подписью, уничтожением и обратным обращением к идентичному типу (последнее редко используется).

Свободные реализации стирания типа на основе хранилища получают семантику перемещения «бесплатно» путем обмена вокруг указателей.

В вашем случае вы захотите создать «достаточно большое» выровненное хранилище в типе. Вы хотите набрать стирание, чтобы скопировать, получить-как-ссылку-на-базу, уничтожить и, вероятно, двигаться (как вы храните внутренне).

std::aligned_storage предназначен для выполнения вашей задачи (вы можете передать требования к выравниванию всех типов, которые вы собираетесь хранить). Затем на месте новый объект.

Создайте таблицу операций, которые вы хотите выполнить на объекте, с помощью void* - копировать, перемещать, уничтожать и конвертировать в base*.

template<class Sig>using f = Sig*; 

struct table { 
    f<base*(void*)>    convert_to_base; 
    f<base const*(void const*)> convert_to_const_base; 
    f<void(void*,void const*)> copy_construct; 
    f<void(void*)>    destroy; 
    f<void(void*,void*)>  move_construct; 
}; 
template<class T> 
table const* get_table() { 
    static table t = { 
    // convert to base: 
    [](void*p)->base*{ 
     T*t=static_cast<T*>(p); 
     return t; 
    }, 
    // convert to const base: 
    [](void const*p)->base const*{ 
     T const*t=static_cast<T const*>(p); 
     return t; 
    }, 
    // etc 
    }; 
    return &t; 
} 

теперь хранить get_table<T>() в вашем экземпляре типа стертым (это в основном таблица виртуальных функций, реализуемый вручную), и написать оберточной регулярный класс, чтобы использовать функции из table манипулировать aligned_storage<?...>.

Теперь это можно сделать с помощью boost::variant или с помощью чего-то типа моего some, который действует как any без использования хранилища кучи. Некоторая ссылка включает в себя реализацию, которая компилирует вышеприведенную технику таблицы псевдовиртуальной функции. Однако я, вероятно, неправильно использовал выровненное хранилище, поэтому будьте осторожны.

2

Вы можете использовать std :: aligned_storage, чтобы обернуть ваши классы. Если предположить, что Derived2 является крупнейшим:

class Storage 
{ 
public: 
    Storage(int data, int data2) 
    { 
    new (&data) Derived1(data, data2); 
    } 
    Storage(int data, float data2) 
    { 
    new (&data) Derived2(data, data2); 
    } 
    Base* getBase() 
    { 
    return reinterpret_cast<Base*>(&data); 
    } 
    ~Storage() 
    { 
    getBase()->Base::~Base(); 
    } 
private: 
    std::aligned_storage<sizeof(Derived2)> data; 
}; 
+0

Кастинг указатель на буфер, созданный как 'Derived1 'to' Base * '- это неопределенное поведение в общем случае. Есть даже тонкие практические случаи, когда он ломается. Если вы хотите безопасно конвертировать в «Base *», сначала переведите интерпретацию в правильный «DerivedX *», затем неявный или статический приведение в 'Base *'. – Yakk

+0

@ Якк, конечно, я не собирался давать универсальное решение, просто прост в понимании проекта для конкретной задачи. Ваш подход более фундаментален. – Nikerboker

Смежные вопросы