Столкнулся вот с какой ситуацией. Нужно создать объект типа 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. }; |
Только вот не уверен, что такой дополнительный уровень контроля будет кому-нибудь нужен.
В общем, любые соображения и любая критика приветствуется. Есть ощущение, что можно сделать полезные повтороиспользуемый класс.
template<typename T> class buffer_allocated_object { alignas(T) std::array<char, sizeof(T)> buffer_; bool allocated_{ false }; void destroy_if_allocated() { if(allocated_) { get()->~T(); allocated_ = false; } } public : using pointer = T*; using element_type = T; using reference = typename std::add_lvalue_reference<T>::type; buffer_allocated_object() noexcept = default; buffer_allocated_object(const buffer_allocated_object &) = delete; buffer_allocated_object(buffer_allocated_object &&) = delete; ~buffer_allocated_object() { destroy_if_allocated(); } template<typename... Args> void allocate(Args &&...args) { destroy_if_allocated(); new(buffer_.data()) T(std::forward<Args>(args)...); allocated_ = true; } void destroy() { destroy_if_allocated(); } operator bool() const noexcept { return allocated_; } pointer get() const noexcept { return reinterpret_cast<pointer>(const_cast<char *>(buffer_.data())); } pointer operator->() const noexcept { return get(); } reference operator*() const noexcept { return *get(); } }; |
Комментариев нет:
Отправить комментарий