Законно ли обходить конструктор класса или это приводит к неопределенному поведению?

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

class C
{
public:
    int* x;
};

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

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

(Наткнулся на это с помощью другого вопроса, совершенно другого; спрашиваю о любопытстве...)

6 ответов

Решение

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

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

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

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, является свойством объекта, а не типа, поэтому "существует объект, потому что его инициализация пуста / тривиальна "назад.

Это законно сейчас и задним числом, начиная с C++98!

Действительно, формулировка спецификации C++ до C++20 определяла объект как (например, формулировка C++17, [intro.object]):

Конструкции в программе C++ создают, уничтожают, обращаются к объектам, обращаются к ним и манипулируют ими. Объект создается определением (6.1), новым выражением (8.5.2.4), при неявном изменении активного члена объединения (12.3) или при создании временного объекта (7.4, 15.2).

Возможность создания объекта с использованием выделения памяти не упоминалась. Делает это де-факто неопределенным поведением.

Затем это было рассмотрено как проблема, и эта проблема была позже решена на https://wg21.link/P0593R6 и принята как DR для всех версий C++, начиная с C++98 включительно, а затем добавлена ​​в спецификацию C++20, в новой редакции:

[intro.object]

  1. Конструкции в программе C++ создают, уничтожают, обращаются к объектам, обращаются к ним и манипулируют ими. Объект создается определением, новым выражением, операцией, которая неявно создает объекты (см. Ниже)...

...

  1. Кроме того, после неявного создания объектов в указанной области хранения некоторые операции описываются как создание указателя на подходящий созданный объект. Эти операции выбирают один из неявно созданных объектов, адрес которого является адресом начала области хранения, и создают значение указателя, указывающее на этот объект, если это значение приведет к тому, что программа будет иметь определенное поведение. Если такое значение указателя не даст поведение, определяемое программой, поведение программы не определено. Если несколько таких значений указателя будут давать поведение, определяемое программой, не указано, какое значение указателя создается.

Пример приведен в C++20 спецификации является:

#include <cstdlib>
struct X { int a, b; };
X *make_x() {
   // The call to std​::​malloc implicitly creates an object of type X
   // and its subobjects a and b, and returns a pointer to that X object
   // (or an object that is pointer-interconvertible ([basic.compound]) with it), 
   // in order to give the subsequent class member access operations   
   // defined behavior. 
   X *p = (X*)std::malloc(sizeof(struct X));
   p->a = 1;   
   p->b = 2;
   return p;
}

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

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

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

Далее в тех же абзацах есть этот пример:

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

Этот код помечен как UB, когда X нетривиально, но как не UB, когда X тривиально.

В большинстве случаев обход конструктора обычно приводит к неопределенному поведению.

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

[basic.life] / 1

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

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

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

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

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

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

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

  2. vtable указатели не могут манипулировать кодом вообще

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

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

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

struct C
{
   int *x;
};

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

Разве это не выглядит знакомым? Все хорошо. Там нет проблем с этим кодом.

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

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

Другие вопросы по тегам