Остерегайтесь, мы оборачиваем логово дракона.
Рассмотрим следующие два класса:
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, построенный как частьFooat(3), ничего не делает;
И поэтому Foo получает инициализированный указатель, и все хорошо.
Конечно, это то, что происходит на практике, даже при -O3 (смотрите сами!).
Но безопасно ли это, или дракон схватит и съест меня однажды?