2008-09-03 10 views
30

Как правило, я предпочитаю использовать значение, а не семантику указателя в C++ (то есть используя vector<Class> вместо vector<Class*>). Обычно небольшая потеря производительности более чем компенсируется тем, что не нужно запоминать удаленные динамически распределенные объекты.Могу ли я иметь полиморфные контейнеры со значениями семантики в C++?

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

#include <iostream> 

using namespace std; 

class Parent 
{ 
    public: 
     Parent() : parent_mem(1) {} 
     virtual void write() { cout << "Parent: " << parent_mem << endl; } 
     int parent_mem; 
}; 

class Child : public Parent 
{ 
    public: 
     Child() : child_mem(2) { parent_mem = 2; } 
     void write() { cout << "Child: " << parent_mem << ", " << child_mem << endl; } 

     int child_mem; 
}; 

int main(int, char**) 
{ 
    // I can have a polymorphic container with pointer semantics 
    vector<Parent*> pointerVec; 

    pointerVec.push_back(new Parent()); 
    pointerVec.push_back(new Child()); 

    pointerVec[0]->write(); 
    pointerVec[1]->write(); 

    // Output: 
    // 
    // Parent: 1 
    // Child: 2, 2 

    // But I can't do it with value semantics 

    vector<Parent> valueVec; 

    valueVec.push_back(Parent()); 
    valueVec.push_back(Child()); // gets turned into a Parent object :(

    valueVec[0].write();  
    valueVec[1].write();  

    // Output: 
    // 
    // Parent: 1 
    // Parent: 2 

} 

Мой вопрос: Могу ли я иметь мой торт (значение семантики) и съесть его (полиморфные контейнеры)? Или мне нужно использовать указатели?

ответ

22

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

Одно разумное решение - хранить безопасные интеллектуальные указатели контейнера. Обычно я использую boost :: shared_ptr, который безопасен для хранения в контейнере. Обратите внимание, что std :: auto_ptr нет.

vector<shared_ptr<Parent>> vec; 
vec.push_back(shared_ptr<Parent>(new Child())); 

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

+3

`boost :: ptr_vector` часто является более дешевой и простой альтернативой` std :: vector > ` – ben 2008-09-16 16:34:41

+4

Этот ответ не рассматривает семантику значений. shared_ptr предоставляет ссылочную семантику над классами, полученными из T, для instace, shared_ptr a, b; b.reset (новый Derived1); a = b; не делает копию объекта Derived1. – Aaron 2008-09-17 05:28:25

2

Посмотрите на static_cast и reinterpret_cast
В C++ Язык программирования, 3-е изд, Бьярне Страуструп описывает его на странице 130. Там целый раздел об этом в главе 6.
Вы можете переделка ваш класс родительского класса. Это требует, чтобы вы знали, когда каждый из них. В книге доктор Страуструп рассказывает о различных методах, чтобы избежать этой ситуации.

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

3

Вы также можете рассмотреть boost::any. Я использовал его для гетерогенных контейнеров. При чтении значения обратно вам нужно выполнить any_cast. Это приведет к ошибке bad_any_cast, если он не сработает. Если это произойдет, вы можете перехватить и перейти к следующему типу.

I поверьте, это вызовет bad_any_cast, если вы попробуете any_cast производный класс к его базе. Я пробовал:

// But you sort of can do it with boost::any. 

    vector<any> valueVec; 

    valueVec.push_back(any(Parent())); 
    valueVec.push_back(any(Child()));  // remains a Child, wrapped in an Any. 

    Parent p = any_cast<Parent>(valueVec[0]); 
    Child c = any_cast<Child>(valueVec[1]); 
    p.write(); 
    c.write(); 

    // Output: 
    // 
    // Parent: 1 
    // Child: 2, 2 

    // Now try casting the child as a parent. 
    try { 
     Parent p2 = any_cast<Parent>(valueVec[1]); 
     p2.write(); 
    } 
    catch (const boost::bad_any_cast &e) 
    { 
     cout << e.what() << endl; 
    } 

    // Output: 
    // boost::bad_any_cast: failed conversion using boost::any_cast 

Все, что было сказано, я бы также пойти по пути shared_ptr первым! Просто подумал, что это может быть интересно.

3

Большинство типов контейнеров хотят абстрагироваться от конкретной стратегии хранения, будь то связанный список, вектор, на основе дерева или что у вас есть. По этой причине у вас возникнут проблемы с тем, как обладать и потреблять вышеупомянутый торт (т. Е. Торт - ложь (NB: кто-то должен был сделать эту шутку)).

Итак, что делать? Ну, есть несколько милых опций, но большинство из них сводится к вариантам по одной из нескольких тем или их комбинаций: сбор или придумывание подходящего умного указателя, игра с шаблонами или шаблонами шаблонов каким-то умным способом, с использованием общего интерфейса для сдержанных который обеспечивает крючок для реализации двойной отправки.

Между двумя вашими поставленными целями существует основное напряжение, поэтому вы должны решить, что хотите, а затем попытаться создать что-то, что позволит вам в основном то, что вы хотите. Это - это, чтобы сделать несколько приятных и неожиданных трюков, чтобы получить указатели, чтобы они выглядели как ценности с достаточно умным подсчетом ссылок и достаточно умными реализациями фабрики. Основная идея состоит в том, чтобы использовать подсчет ссылок и копировать по требованию и константу и (для фактора) комбинацию препроцессора, шаблонов и статических правил инициализации C++, чтобы получить что-то настолько умное, насколько это возможно, о автоматизации конверсий указателей.

Прошло некоторое время, пытаясь представить, как использовать Virtual Proxy/Envelope-Letter/этот симпатичный трюк со ссылками подсчитанных указателей, чтобы выполнить что-то вроде основы для семантического программирования значений в C++.

И я думаю, что это можно сделать, но вам нужно будет предоставить довольно закрытый, подобный C# мир в C++ (хотя тот, из которого вы могли бы прорваться к базовому C++, когда это необходимо). Поэтому у меня есть много симпатии к вашей линии мысли.

2

Просто добавьте одну вещь для всех 1800 INFORMATION уже сказано.

Возможно, вы захотите взглянуть на "More Effective C++" Скотта Майерса «Пункт 3: Никогда не обрабатывайте массивы полиморфно», чтобы лучше понять эту проблему.

10

Да, вы можете.

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

10

Я просто хотел указать, что вектор <Foo> обычно более эффективен, чем вектор < Foo * >. В векторе <Foo> все Foos будут смежными друг с другом в памяти. Предполагая, что холодный TLB и кеш, первое чтение добавит страницу в TLB и потянет кусок вектора в кеши L #; последующие чтения будут использовать теплый кеш и загруженные TLB, с случайными промахами кеша и менее частыми ошибками TLB.

Контрастируйте это с помощью вектора < Foo * >: Когда вы заполняете вектор, вы получаете Foo * из своего распределителя памяти. Предполагая, что ваш распределитель не очень умный, (tcmalloc?) Или вы медленно заполняете вектор, местоположение каждого Foo, вероятно, будет далеко от других Foos: возможно, всего на сотни байтов, может быть, на мегабайтах друг от друга.

В худшем случае, как вы просканировать вектор < Foo * > и разыменования каждого указателя вы будете нести TLB ошибки и кэш-промах - это будет в конечном итоге является много медленнее, чем если бы вы имели вектор <Foo>. (Ну, в самом худшем случае каждый Foo был выгружен на диск, и каждое чтение берет на себя задачу seek() и read(), чтобы переместить страницу обратно в ОЗУ.)

Итак, продолжайте использовать вектор <Foo> всякий раз, когда это необходимо. :-)

1

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

Это идея, которая может удовлетворить ваши потребности.