вторник, 26 сентября 2017 г.

[prog.c++] Строю лисапед на тему аллокации объекта на уже имеющемся буфере

Столкнулся вот с какой ситуацией. Нужно создать объект типа 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<charsizeof(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.
};

Только вот не уверен, что такой дополнительный уровень контроля будет кому-нибудь нужен.

В общем, любые соображения и любая критика приветствуется. Есть ощущение, что можно сделать полезные повтороиспользуемый класс.