Ручное построение тривиального базового класса с помощью размещения-new

Остерегайтесь, мы оборачиваем логово дракона.

Рассмотрим следующие два класса:

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 (смотрите сами!).
Но безопасно ли это, или дракон схватит и съест меня однажды?

Ответы

Ответ 1

Кажется, что стандарт явно запрещен. Завершение жизни объекта и запуск новых объектов время жизни в том же месте явно разрешено, если это базовый класс:

§3.8 Срок службы объекта

§3.8.7 - Если после окончания срока жизни объекта и до хранения который занятый объект повторно используется или освобождается, новый объект созданный в месте хранения, в котором находился исходный объект, указатель, указывающий на исходный объект, ссылка, к исходному объекту, или имя исходного объекта будет автоматически ссылаются на новый объект и, как только время жизни новый объект, который можно использовать для управления новым объектом, , если:

  • хранилище для нового объекта точно накладывает место хранения который первоначальный объект занял, и

  • новый объект тот же тип, что и исходный объект (игнорируя верхний уровень cv-qualifiers) и

  • [snip] и

  • исходный объект был наиболее производным объектом (1.8) типа T и новый объект является самым производным объектом типа T (т.е. они не являются субобъекты базового класса).