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

Это продолжение моего предыдущего вопроса, где я, кажется, сделал проблему более сложной, чем предполагал изначально. (См. Обсуждения в вопросе и ответы на комментарии там.) Этот вопрос представляет собой небольшую модификацию исходного вопроса, устраняющую проблему особых правил во время строительства / разрушения ограждающего объекта.


Разрешено ли повторно использовать хранилище нестатического элемента данных в течение всего времени существования включающего его объекта, и если да, то при каких условиях?

Рассмотрим программу

#include<new>
#include<type_traits>

using T = /*some type*/;
using U = /*some type*/;

static_assert(std::is_object_v<T>);
static_assert(std::is_object_v<U>);
static_assert(sizeof(U) <= sizeof(T));
static_assert(alignof(U) <= alignof(T));

struct A {
    T t /*initializer*/;
    U* u;

    void construct() {
        t.~T();
        u = ::new(static_cast<void*>(&t)) U /*initializer*/;
    }

    void destruct() {
        u->~U();
        ::new(static_cast<void*>(&t)) T /*initializer*/;
    }

    A() = default;
    A(const A&) = delete;
    A(A&&) = delete;
    A& operator=(const A&) = delete;
    A& operator=(A&&) = delete;
};

int main() {
    auto a = new A;
    a->construct();
    *(a->u) = /*some assignment*/;
    a->destruct(); /*optional*/
    delete a; /*optional*/

    A b; /*alternative*/
    b.construct(); /*alternative*/
    *(b.u) = /*some assignment*/; /*alternative*/
    b.destruct(); /*alternative*/
}

Помимо static_asserts предполагаем, что инициализаторы, деструкторы и присваивания T и U не выбрасываются.

Какие условия должны дополнительно удовлетворять типы объектов T и U, чтобы программа имела определенное поведение, если таковое имеется?

Зависит ли это от фактического вызова деструктора A (например, присутствуют ли строки /*optional*/ или /*alternative*/)?

Зависит ли это от продолжительности хранения A, например используются ли вместо этого /*alternative*/ строк в main?


Обратите внимание, что программа не использует член t после нового размещения, за исключением деструктора и функции destruct. Конечно, использование его, когда его хранилище занято другим типом, не допускается.

Также обратите внимание, что программа создает объект исходного типа в t до того, как его деструктор вызывается во всех путях выполнения, поскольку я запретил T и U генерировать исключения.


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


person walnut    schedule 08.12.2019    source источник
comment
Вам, вероятно, следует указать конкретную версию C ++ std для этих прекрасных вопросов.   -  person curiousguy    schedule 12.12.2019
comment
@curiousguy Я пометил C ++ 20. Я предполагаю, что он достаточно стабилен, поэтому маловероятно, что ответ будет признан недействительным до того, как он будет завершен. В противном случае я бы выбрал C ++ 17.   -  person walnut    schedule 12.12.2019
comment


Ответы (2)


Если a уничтожен (из-за delete или выпадения из области видимости), то вызывается t.~T(), что является UB, если t на самом деле не является T (не вызывая destruct).

Это не применяется, если

После вызова destruct вам не разрешается использовать t, если T имеет const или ссылочные элементы (до C ++ 20).

Кроме того, насколько я понимаю, нет никаких ограничений на то, что вы делаете с классом, как написано.

person Rakete1111    schedule 09.12.2019
comment
Мне кажется, что ограничение на использование t после destruct также применимо к неявному вызову деструкции. Будет ли тогда член const в T уничтожить A UB? - person walnut; 09.12.2019

Этот ответ основан на черновике, доступном по адресу http://eel.is/c++draft/

Мы можем попытаться применить (проверяя каждое условие) то, что я решил назвать предложением «объект нежити», к любому предыдущему объекту, который существовал, здесь мы применяем его к члену t типа T:

Срок службы [basic.life] / 8

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

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

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

(8.3) исходный объект не является ни полным объектом, квалифицированным как const, ни подобъектом такого объекта, и

(8.4) ни исходный объект, ни новый объект не являются потенциально перекрывающимися подобъектами ([intro.object]).

Условия 1 и 2 автоматически гарантируются за счет использования нового размещения на старом участнике:

struct A {
    T t /*initializer*/; (...)

    void destruct() { (...)
        ::new(static_cast<void*>(&t)) T /*initializer*/;
    }

Расположение такое же, и тип такой же. Оба условия легко проверяются.

Не создано A объектов:

auto a = new A;
...
A b; /*alternative*/

являются полными объектами с квалификацией const, поэтому t не является членом полного объекта с квалификацией const. Условие 3 выполнено.

Теперь определение потенциально перекрывающихся объектов содержится в объектной модели [intro.object] / 7:

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

(7.1) подобъект базового класса, или

(7.2) нестатический элемент данных, объявленный с атрибутом no_unique_address.

Член t не является ни тем, ни другим, и выполняется условие 4.

Все 4 условия выполнены, поэтому имя члена t можно использовать для наименования нового объекта.

[Обратите внимание, что я даже не упомянул тот факт, что подобъект не является константным членом, а не его подобъектами. Это не часть последнего проекта.

Это означает, что у подобъекта const может быть изменено значение, а у ссылочного члена может быть изменен референт для существующего объекта. Это более чем тревожно и, вероятно, не поддерживается многими компиляторами. Конец примечания.]

person curiousguy    schedule 11.12.2019
comment
Я определенно рассмотрел абзац, который вы цитируете, но не думаю, что это так просто. Например, в другой ветке комментариев я узнал, что [intro.object] / 2 фактически делает объекты, созданные с помощью нового размещения, не подобъектами. Итак, по определению complete object, они будут фактически законченными объектами. Кроме того, возникает вопрос, разрешено ли повторное использование части объекта без завершения времени существования включающего объекта, за исключением вложенных типов. Я думаю, что эти вопросы требуют дополнительных размышлений. - person walnut; 11.12.2019
comment
Затем возникает вопрос, будет ли в цитируемом абзаце исходный объект начальным T, занимающим это хранилище, или промежуточным U при создании нового T? Я предполагаю второе, потому что он говорит перед повторным использованием памяти, занимаемой объектом. - person walnut; 11.12.2019
comment
объекты, созданные с помощью нового размещения, а не подобъектов. Они представляют собой законченный объект, как и все, что создано new, но затем, когда все 4 условия выполнены, имя члена ссылается на них, поэтому они также рассматривается как подобъект. Я согласен, что это проблематично на многих уровнях, но намерение применить предложение к подобъекту (члену или элементу массива) не может быть более ясным, в предложении явно упоминается старый объект, потенциально являющийся подобъектом. - person curiousguy; 11.12.2019
comment
@walnut исходный объект будет начальным T Это тот, потому что он нас беспокоит; в предложении не говорится ни о том, ни о другом, это может быть и то, и другое. Это может быть любой из 100 объектов, если 100 объектов были созданы в одном месте хранения. Нет срока годности объекта un-dead-ism. Вы можете воссоздать любой объект, который раньше был там, в любой момент времени. Вы также можете переключаться между двумя объектами типа T и U. - person curiousguy; 11.12.2019
comment
@walnut Я определенно принял во внимание абзац, который вы цитируете Как человек, ориентированный на языковой юрист, я подозревал, что вы знаете. Но тогда это выглядело как самая важная часть std, поэтому я процитировал ее ... - person curiousguy; 11.12.2019