Является ли обход конструктора класса законным или это приводит к поведению undefined?
Рассмотрим следующий пример кода:
class C
{
public:
int* x;
};
void f()
{
C* c = static_cast<C*>(malloc(sizeof(C)));
c->x = nullptr; // <-- here
}
Если мне приходилось жить с неинициализированной памятью по какой-либо причине (конечно, если это возможно, я бы назвал new C()
), я все равно мог бы назвать конструктор размещения. Но если я опустил это, как указано выше, и инициализировал каждую переменную-член вручную, приведет ли она к поведению undefined? То есть является обход поведения конструктора как такового undefined или же законно заменить вызов его каким-то эквивалентным кодом вне класса?
(Перешел через это через другой вопрос по совершенно другому вопросу, прося любопытство...)
Ответы
Ответ 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
Хотя вы можете инициализировать все явные члены таким образом, вы не можете инициализировать все, что может содержать класс:
То есть, в тот момент, когда у вас есть один виртуальный член или виртуальный базовый класс или ссылочный элемент, нет возможности правильно инициализировать ваш объект, кроме как вызывать его конструктор.
Ответ 6
Я думаю, что это не должно быть UB. Вы указываете указатель на некоторую необработанную память и обрабатываете ее данные определенным образом, здесь нет ничего плохого.
Если конструктор этого класса что-то делает (инициализирует переменные и т.д.), вы снова получите указатель на необработанный, неинициализированный объект, используя который, не зная, что должен был использовать конструктор (по умолчанию) (и повторение его поведения) будет UB.