Подтвердить что ты не робот

Является ли обход конструктора класса законным или это приводит к поведению undefined?

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

class C
{
public:
    int* x;
};

void f()
{
    C* c = static_cast<C*>(malloc(sizeof(C)));
    c->x = nullptr; // <-- here
}

Если мне приходилось жить с неинициализированной памятью по какой-либо причине (конечно, если это возможно, я бы назвал new C()), я все равно мог бы назвать конструктор размещения. Но если я опустил это, как указано выше, и инициализировал каждую переменную-член вручную, приведет ли она к поведению undefined? То есть является обход поведения конструктора как такового undefined или же законно заменить вызов его каким-то эквивалентным кодом вне класса?

(Перешел через это через другой вопрос по совершенно другому вопросу, прося любопытство...)

4b9b3361

Ответ 1

Нет живого объекта C, поэтому притворяясь, что есть один результат в поведении undefined.

P0137R1, принятый на заседании комитета Оулу, позволяет это понять, определяя объект следующим образом ([intro.object]/1):

Объект создается определением ([basic.def]), новым выражением ([expr.new]), когда неявно изменяется активный член union ([class.union]) или когда создается временный объект ([conv.rval], [class.temporary]).

reinterpret_cast<C*>(malloc(sizeof(C))) - ничто из этого.

Также см. этот поток std-предложений, с очень похожим примером от Ричарда Смита (с фиксированной опечаткой):

struct TrivialThing { int a, b, c; };
TrivialThing *p = reinterpret_cast<TrivialThing*>(malloc(sizeof(TrivialThing))); 
p->a = 0; // UB, no object of type TrivialThing here

Кода [basic.life]/1 применяется только тогда, когда объект создается в первую очередь. Обратите внимание, что "тривиальный" или "пустой" (после изменения терминологии, сделанного CWG1751), поскольку этот термин используется в [basic. life]/1, является свойством объекта, а не типа, поэтому "есть объект, потому что его инициализация пуста/тривиальна" назад.

Ответ 2

Я думаю, что код в порядке, если у типа есть тривиальный конструктор, как ваш. Использование объекта, отличного от malloc без вызова места размещения new, просто использует объект перед вызовом его конструктора. Из стандарта С++ 12.7 [class.dctor]:

Для объекта с нетривиальным конструктором, ссылаясь на любой нестатический член или базовый класс объекта перед тем, как конструктор начинает выполнение, приводит к поведению undefined.

Поскольку исключение доказывает правило, ссылайтесь на нестатический член объекта с тривиальным конструктором до того, как конструктор начнет выполнение не является UB.

Далее в тех же параграфах приведен пример:

extern X xobj;
int* p = &xobj.i;
X xobj;

Этот код обозначается как UB, когда X является нетривиальным, но как не UB, когда X тривиально.

Ответ 3

По большей части обход конструктора в целом приводит к поведению undefined.

Есть некоторые, возможно, угловые случаи для простых старых типов данных, но в любом случае вы ничего не выигрываете, избегая их, конструктор тривиален. Является ли код столь же простым, как и представленным?

[basic.life]/1

Время жизни объекта или ссылки является временем выполнения объекта или ссылки. Говорят, что объект имеет непустую инициализацию, если он относится к классу или агрегату, и он или один из его подобъектов инициализируется конструктором, отличным от тривиального конструктора по умолчанию. [Примечание: инициализация тривиальным конструктором copy/move является пустой функцией инициализации. - end note] Время жизни объекта типа T начинается, когда:

  • сохраняется хранилище с надлежащим выравниванием и размером для типа T и
  • Если объект имеет незапамятную инициализацию, его инициализация завершена.

Время жизни объекта типа T заканчивается, когда:

  • если T - тип класса с нетривиальным деструктором ([class.dtor]), начинается вызов деструктора или
  • хранилище, которое объект занимает, повторно используется или освобождается.

Помимо кода, сложнее читать и рассуждать, вы либо ничего не выиграете, либо приземлитесь с поведением undefined. Просто используйте конструктор, это идиоматический С++.

Ответ 4

Этот конкретный код в порядке, потому что C является POD. Пока C является POD, его также можно инициализировать.

Ваш код эквивалентен этому:

struct C
{
   int *x;
};

C* c = (C*)malloc(sizeof(C)); 
c->x = NULL;

Разве это не похоже на знакомое? Все хорошо. Нет проблем с этим кодом.

Ответ 5

Хотя вы можете инициализировать все явные члены таким образом, вы не можете инициализировать все, что может содержать класс:

  • ссылки не могут быть установлены за пределами списка инициализаторов

  • Указатели vtable не могут управляться кодом вообще

То есть, в тот момент, когда у вас есть один виртуальный член или виртуальный базовый класс или ссылочный элемент, нет возможности правильно инициализировать ваш объект, кроме как вызывать его конструктор.

Ответ 6

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

Если конструктор этого класса что-то делает (инициализирует переменные и т.д.), вы снова получите указатель на необработанный, неинициализированный объект, используя который, не зная, что должен был использовать конструктор (по умолчанию) (и повторение его поведения) будет UB.