Какая привилегированная идиома С++ владеет коллекцией полиморфных объектов?
Рассмотрим следующие классы
class Base {
public:
virtual void do_stuff() = 0;
};
class Derived : public Base {
public
virtual void do_stuff() { std::cout << "I'm useful"; };
};
Теперь скажем, что я хочу иметь еще один класс, ответственный за владение объектами Base
производных типов и перебирать их, называя их метод do_stuff()
. Это похоже на это, но я не знаю, что T
следует объявить как
class Owner {
public:
void do_all_stuff() {
//iterate through all items and call do_stuff() on them
}
void add_item(T item) {
items.push_back(item);
}
vector<T> items;
}
Я вижу несколько возможностей:
T
не может быть Base
, так как я мог бы только добавлять объекты конкретного типа Base
, так что не может быть и речи.
T
может быть Base*
или Base&
, но теперь мне нужно доверять вызывающему устройству add_item()
, чтобы передать мне указатель или ссылку на объект, который будет существовать, когда я его извлечу из items
. Я не могу delete
элементы в деструкторе Owner
, так как я не знаю, что они были динамически распределены. Однако они должны быть delete
'd, если бы они были, что оставляет меня с двусмысленным владением.
T
может быть Base*
или Base&
, и я добавляю метод Base* create_item<DerivedT>() { return new DerivedT; }
к Owner
. Таким образом, я знаю, что указатель останется в силе, и я его сохраню, но я не могу вызвать конструктор не по умолчанию на DerivedT
. Кроме того, Owner
становится ответственным за создание экземпляров объектов. Я также должен удалить каждый элемент в деструкторе Owner
, хотя это и не проблема.
В принципе, я хотел бы сделать что-то похожее на:
Owner owner;
void add_one() {
Derived d;
owner.add_item(d);
}
void ready() {
owner.do_all_stuff();
}
void main() {
for(int i = 0; i < 10; ++i) {
add_one();
}
ready();
}
Я уверен, что там что-то связано с семантикой перемещения (я мог перемещать объекты, переданные в add_items()
, чтобы их владеть), но я до сих пор не могу понять, как будет объявлена моя коллекция.
Что такое идиома С++ для такого рода полиморфного владения (в частности, с контейнерами STL)?
Ответы
Ответ 1
Полиморфные объекты должны обрабатываться указателем или ссылкой. Поскольку их время жизни, вероятно, не связано с определенной областью, у них также, вероятно, будет динамическая длительность хранения, а это значит, что вы должны использовать интеллектуальный указатель.
Умные указатели, такие как std::shared_ptr
и std::unique_ptr
, отлично работают в стандартных типах коллекций.
std::vector<std::unique_ptr<Base>>
Использование этого в Owner
выглядит следующим образом:
class Owner {
public:
void do_all_stuff() {
//iterate through all items and call do_stuff() on them
}
void add_item(std::unique_ptr<Base> item) {
items.push_back(std::move(item));
}
vector<std::unique_ptr<Base>> items;
}
Тип аргумента add_item
определяет политику владения, необходимую для добавления элемента, и требует от пользователя выходить из своего пути, чтобы свернуть его. Например, они не могут случайно передать необработанный указатель с некоторой неявной, несовместимой семантикой собственности, потому что unique_ptr
имеет явный конструктор.
unique_ptr
также позаботится об удалении объектов, принадлежащих Owner
. Хотя вам нужно убедиться, что Base
имеет виртуальный деструктор. С вашим текущим определением вы получите поведение undefined. Полиморфные объекты всегда должны иметь виртуальный деструктор.
Ответ 2
Предполагая из вашего контекста, что Owner
является единственным владельцем содержащихся объектов, T
должен быть unique_ptr<Base>
(где unique_ptr
происходит от boost или std в зависимости от доступности С++ 11). Это правильно признает, что оно принадлежит только контейнеру и дополнительно отображает семантику передачи собственности в ваш вызов add_item
.
Ответ 3
Другие альтернативы, заслуживающие рассмотрения, - использовать boost::ptr_container
или, что еще лучше, использовать библиотеку типа adobe::poly
или boost::type_erasure
для ваших полиморфных типов, использовать полиморфизм во времени выполнения на основе стоимости - избегать необходимости в указателях, наследование и т.д.
Ответ 4
На самом деле вы не можете хранить ссылки в STL, только указатели или реальные значения.
Таким образом, T является базой *
Попробуйте другие вещи, которые вы будете жаловаться компилятору.