Размещение нового базового подобъекта производного в C++

Определено ли поведение для размещения-нового тривиально разрушаемого базового объекта производного?

struct base { int& ref; };
struct derived : public base {
    complicated_object complicated;
    derived(int& r, complicated_arg arg) :
            base {r}, complicated(arg) {}
};

unique_ptr<derived> rebind_ref(unique_ptr<derived>&& ptr,
                               int& ref) {
    // Change where the `ref` in the `base` subobject of
    // derived refers.
    return unique_ptr<derived>(static_cast<derived*>(
        ::new (static_cast<base*>(ptr.release()) base{ref}));
}

Обратите внимание, что я попытался структурировать rebind_ref так, чтобы не нарушать каких-либо строгих предположений об алиасинге, которые мог бы сделать компилятор.


person Filipp    schedule 19.10.2018    source источник
comment
почему бы не использовать int *ref в этом случае. производный класс может быть написан, предполагая, что ref никогда не меняется.   -  person user685684    schedule 19.10.2018
comment
Мой вопрос не о перепривязке ссылки. Речь идет о построении нового значения поверх базового подобъекта производного.   -  person Filipp    schedule 19.10.2018
comment
Повторное использование памяти завершает время жизни объекта.   -  person curiousguy    schedule 21.10.2018


Ответы (2)


Нет, это не разрешено стандартом C++ по крайней мере по двум причинам.

Текст, который иногда допускает размещение нового объекта в хранилище для другого объекта того же типа, находится в [basic.life], пункт 8. Смелый акцент мой.

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

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

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

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

  • [C++17] исходный объект был наиболее производным объектом типа T, а новый объект является наиболее производным объектом типа T (то есть они не являются подобъектами базового класса< /сильный>).

  • [C++20 draft 2018-10-09] ни исходный объект, ни новый объект не являются потенциально перекрывающимися подобъектами ([intro.object]).

Изменение C++20 должно учитывать возможность нестатических членов данных нулевого размера, но оно по-прежнему исключает все подобъекты базового класса (пустые или нет). Потенциально перекрывающийся подобъект — это новый термин, определенный в [intro.object] параграф 7:

К потенциально перекрывающимся подобъектам относятся:

  • подобъект базового класса или

  • нестатический член данных, объявленный с атрибутом no_unique_address ([dcl.attr.nouniqueaddr]).

(Даже если вы найдете какой-то способ изменить порядок вещей, чтобы избежать проблем с эталонным членом и базовым классом, не забудьте убедиться, что никто никогда не сможет определить переменную const derived, например, сделав все конструкторы закрытыми!)

person aschepler    schedule 21.10.2018
comment
Спасибо за отличный ответ. ... указатель, указывающий на исходный объект, ссылка, указывающая на исходный объект, или имя исходного объекта будут автоматически ссылаться на новый объект... интересно. Является ли результат выражения "placement-new" указателем, указывающим на исходный объект, или указателем на вновь созданный объект (несмотря на то, что он занимает ту же ячейку памяти)? - person Filipp; 22.10.2018
comment
Результат любого выражения new, включая размещение new, указывает на созданный объект. В этом предложении говорится о ранее существовавших указателях и ссылках. - person aschepler; 22.10.2018
comment
Я тщательно структурировал rebind_ref так, чтобы он потреблял старый указатель и возвращал новый указатель, который является результатом нового выражения размещения static_casted в derived. Достаточно ли этого, чтобы избежать указателя, указывающего на исходное правило объекта? - person Filipp; 22.10.2018
comment
@Filipp Подобъект базового класса на самом деле не является указателем, ссылкой или именем. Но в стандарте нет необходимости разъяснять, как ведет себя производный объект после замены подобъекта базового класса, потому что в нем изначально говорится, что такая замена всегда недействительна. - person aschepler; 24.10.2018
comment
На самом деле разделы стандарта, которые вы цитировали выше, говорят только о том, можно ли использовать указатель, ссылку или имя, ссылающееся на объект, который ранее занимал некоторое хранилище, для управления созданным там новым объектом. В С++ 17 я бы использовал std::launder для создания нового имени. К сожалению, у меня есть доступ только к C++14. Имеет ли новое размещение std::launder возможности до C++17? - person Filipp; 25.10.2018

static_cast<derived*>(
        ::new (&something) base{ref})

недействителен, по определению new (...) base(...) создает объект base как новый завершенный объект, который иногда может считаться существующим завершенным объектом или подобъектом member (при условиях, которые base в любом случае не выполняются), но никогда базовый подобъект.

Не существует правила, согласно которому вы можете притворяться, что new (addr) base создает действительный производный объект только потому, что объект base перезаписывает другой базовый подобъект base. Если ранее был объект derived, вы только что повторно использовали его хранилище с new (addr) base. Даже если каким-то волшебным образом объект derived все еще существовал, результат вычисления нового выражения не указывал бы на него, а указывал бы на base завершенный объект.

Если вы хотите притвориться, что сделали что-то (например, создали объект derived), но на самом деле этого не сделали (вызвали конструктор derived), вы можете добавить несколько квалификаторов volatile к указателям, чтобы заставить компилятор стереть все предположения о значениях и скомпилировать код как если был переход ABI.

person curiousguy    schedule 21.10.2018
comment
Где бы вы порекомендовали мне добавить квалификаторы volatile, чтобы сделать то, что я делаю, не UB, не оказывая негативного влияния на то, как компилятор может оптимизировать этот код? - person Filipp; 23.10.2018
comment
@Filipp Целью volatile является удаление информации из компилятора: значение объекта volatile, похоже, исходит из отдельно скомпилированного модуля (и, возможно, из другого компилятора и языка). Стандарт вообще не распространяется на раздельную компиляцию или смешивание разных языков, даже C. - person curiousguy; 23.10.2018