Столкнулся вот с какой ситуацией. Нужно создать объект типа Foo (при этом Foo принципиально не является Copyable и Moveable). Одним из полей объекта Foo должен быть объект типа Bar. Но фокус в том, что объект Bar должен создаваться не сразу при создании объекта Foo, а позже. При этом не факт, что Bar вообще может быть DefaultConstructible. Т.е. не получится написать в лоб:
class Foo { ... // bla-bla-bla. Bar bar_; public: Foo() = default; ... // bla-bla-bla. }; |
Само собой напрашивается использование динамической памяти и unique_ptr:
class Foo { ... // bla-bla-bla. std::unique_ptr<Bar> bar_; public: Foo() = default; ... template<typename... Bar_Constructor_Args> void activate(Bar_Constructor_Args &&...args) { bar_ = std::make_unique<Bar>(std::forward<Bar_Constructor_Args>(args)...); } ... // bla-bla-bla. }; |
Но здесь смущает то, что размер-то для Bar-а уже известен. И не хочется дергать динамическую память для того, чтобы сконструировать Bar в Foo::activate.
Поэтому напрашивается решение с буфером для хранения экземпляра Bar и использование placement new для конструирования нового экземпляра Bar внутри этого буфера. Что-то вроде:
class Foo { ... // bla-bla-bla. alignas(Bar) std::array<char, sizeof(Bar)> bar_buffer_; bool bar_created_{false}; public: Foo() = default; ~Foo() { if(bar_created_) reinterpret_cast<Bar *>(bar_buffer_.data())->~Bar(); } ... template<typename... Bar_Constructor_Args> void activate(Bar_Constructor_Args &&...args) { new(bar_buffer_.data()) Bar(std::forward<Bar_Constructor_Args>(args)...); bar_created_ = true; } ... // bla-bla-bla. }; |
Тут мы получаем те же фишки, что и при использовании unique_ptr, но без обращения к хипу. Но вот эта ручная работа с placement new... Это не есть хорошо.
Поэтому рождается лисапед под названием buffer_allocated_object, код которого приведен под катом. Использовать его предполагается вот таким образом:
class Foo { ... // bla-bla-bla. buffer_allocated_object<Bar> bar_; public: Foo() = default; ... template<typename... Bar_Constructor_Args> void activate(Bar_Constructor_Args &&...args) { bar_.allocate(std::forward<Bar_Constructor_Args>(args)...); } ... // bla-bla-bla. }; |
В общем, к чему я это все? Может кто-то делал для себя что-то подобное или видел где-то что-то готовое на эту же тему? Поделитесь плиз, ссылками и/или опытом.
У меня сейчас самая первая, накиданная по-быстрому реализация. За некий образец брался std::unique_ptr (но без поддержки кастомных делетеров, естественно). Поэтому у меня сейчас метод get() и иже с ним объявлены как константные. Хотя, есть ощущение, что для buffer_allocated_object это неправильно. Нужно иметь две группы таких объектов: и для константного buffer_allocated_object, и для неконстантного.
Еще сейчас buffer_allocated_object не Swappable, не Copyable и не Moveable. Думаю, что он и должен оставаться не Copyable. А вот на счет Moveable и Swappable я что-то сомневаюсь. Может таки сделать их, если тип T является Moveable?
А еще есть идея добавить второй параметр для шаблона buffer_allocated_object. Который будет определять, должны ли методы get() и Ко проверять флаг allocated_. Если должны, то в run-time будут выполняться проверки и будет бросаться исключение при попытке разыменовать указатель на неаллоцированный объект. Что-то вроде:
class Foo { ... // bla-bla-bla. buffer_allocated_object<Bar, checked_access> bar_; public: Foo() = default; ... void do_something() { bar_->do_something(); // Exception if bar_ is not allocated yet. } ... // bla-bla-bla. }; |
Только вот не уверен, что такой дополнительный уровень контроля будет кому-нибудь нужен.
В общем, любые соображения и любая критика приветствуется. Есть ощущение, что можно сделать полезные повтороиспользуемый класс.