Остерегайтесь, мы оборачиваем логово дракона.
Рассмотрим следующие два класса:
struct Base {
std::string const *str;
};
struct Foo : Base {
Foo() { std::cout << *str << "\n"; }
};
Как вы можете видеть, я обращаюсь к неинициализированному указателю. Или я?
Предположим, что я работаю только с Base
классами trivial, не более чем (потенциально вложенными) мешками указателей.
static_assert(std::is_trivial<Base>{}, "!");
Я хотел бы построить Foo
в три этапа:
-
Выделить исходное хранилище для
Foo
-
Инициализируйте подходящий объект
Base
с помощью размещения-new -
Построить
Foo
с помощью размещения-new.
Моя реализация такова:
std::unique_ptr<Foo> makeFooWithBase(std::string const &str) {
static_assert(std::is_trivial<Base>{}, "!");
// (1)
auto storage = std::make_unique<
std::aligned_storage_t<sizeof(Foo), alignof(Foo)>
>();
Foo * const object = reinterpret_cast<Foo *>(storage.get());
Base * const base = object;
// (2)
new (base) Base{&str};
// (3)
new (object) Foo();
storage.release();
return std::unique_ptr<Foo>{object};
}
Так как Base
тривиально, я понимаю, что:
-
Пропуск тривиального деструктора
Base
, построенного в(2)
, прекрасен; -
Тривиальный конструктор по умолчанию в подобъекте
Base
, построенный как частьFoo
at(3)
, ничего не делает;
И поэтому Foo
получает инициализированный указатель, и все хорошо.
Конечно, это то, что происходит на практике, даже при -O3 (смотрите сами!).
Но безопасно ли это, или дракон схватит и съест меня однажды?