规避 class' 构造函数是合法的还是会导致未定义的行为?

Is circumventing a class' constructor legal or does it result in undefined behaviour?

考虑以下示例代码:

class C
{
public:
    int* x;
};

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

如果出于任何原因我不得不忍受未初始化的内存(当然,如果可能的话,我会改为调用 new C()),我仍然 可以 调用放置构造函数。但是如果我像上面那样省略这个,并手动初始化 every 成员变量,它会导致未定义的行为吗? IE。是在规避构造函数本身未定义的行为,还是用 class?

之外的等效代码替换调用它是否合法

(通过另一个关于完全不同问题的问题发现了这个问题;出于好奇...)

我觉得应该不是UB。你让你的指针指向一些原始内存并以特定方式处理它的数据,这里没有什么不好的。

如果此 class 的构造函数执行某些操作(初始化变量等),您将再次得到一个指向原始未初始化对象的指针,使用它时不知道(默认)是什么构造函数应该做的(并重复它的行为)将是 UB。

在大多数情况下,绕过构造函数通常会导致未定义的行为

有一些,可以说,普通旧数据类型的极端情况,但无论如何你一开始就不会赢得任何避免它们的机会,构造函数是微不足道的。代码是否像介绍的那样简单?

[basic.life]/1

The lifetime of an object or reference is a runtime property of the object or reference. An object is said to have non-vacuous initialization if it is of a class or aggregate type and it or one of its subobjects is initialized by a constructor other than a trivial default constructor. [ Note: initialization by a trivial copy/move constructor is non-vacuous initialization. — end note ] The lifetime of an object of type T begins when:

  • storage with the proper alignment and size for type T is obtained, and
  • if the object has non-vacuous initialization, its initialization is complete.

The lifetime of an object of type T ends when:

  • if T is a class type with a non-trivial destructor ([class.dtor]), the destructor call starts, or
  • the storage which the object occupies is reused or released.

除了代码更难阅读和推理之外,您要么一无所获,要么会出现未定义的行为。 只需使用构造函数,它是惯用的 C++。

没有活的 C 对象,因此假装有一个会导致未定义的行为。

P0137R1, adopted at the committee's Oulu meeting, makes this clear by defining object as follows ([intro.object]/1):

An object is created by a definition ([basic.def]), by a new-expression ([expr.new]), when implicitly changing the active member of a union ([class.union]), or when a temporary object is created ([conv.rval], [class.temporary]).

reinterpret_cast<C*>(malloc(sizeof(C))) 是其中的 none。

另见 this std-proposals thread,Richard Smith 的一个非常相似的例子(修正了拼写错误):

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 引用仅适用于首先创建对象的情况。请注意 "trivial" 或 "vacuous"(在 CWG1751 完成的术语更改之后)初始化,因为该术语在 [basic.life]/1 中使用,是 属性 一个对象,而不是一个类型,所以 "there is an object because its initialization is vacuous/trivial" 是倒退的。

这个特定的代码很好,因为 C 是一个 POD。只要 C 是一个 POD,它也可以这样初始化。

您的代码等同于:

struct C
{
   int *x;
};

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

是不是很眼熟?一切都很好。这段代码没有问题。

我认为代码没问题,只要类型有一个简单的构造函数,就像你的一样。使用从 malloc 转换的对象而不调用放置 new 只是在调用其构造函数之前使用该对象。来自 C++ 标准 12.7 [class.dctor]:

For an object with a non-trivial constructor, referring to any non-static member or base class of the object before the constructor begins execution results in undefined behavior.

由于异常证明了规则,在构造函数开始执行之前引用具有普通构造函数的对象的非静态成员不是UB.

在同一段落的下方有这个例子:

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

X 不重要时,此代码标记为 UB,但当 X 不重要时,则不标记为 UB。

虽然您可以用这种方式初始化所有显式成员,但您不能初始化 class 可能包含的所有内容:

  1. 不能在初始化列表外设置引用

  2. vtable指针根本不能被代码操作

也就是说,当您拥有单个虚拟成员、虚拟基 class 或引用成员时,除了调用其构造函数外,无法正确初始化您的对象。

现在是合法的,追溯自 C++98!

事实上,C++20 之前的 C++ 规范措辞将对象定义为(例如 C++17 措辞,[intro.object]):

The constructs in a C++ program create, destroy, refer to, access, and manipulate objects. An object is created by a definition (6.1), by a new-expression (8.5.2.4), when implicitly changing the active member of a union (12.3), or when a temporary object is created (7.4, 15.2).

没有提到使用 malloc 分配创建对象的可能性 。使其成为 事实上的 未定义行为。

它是 then viewed as a problem, and this issue was addressed later by https://wg21.link/P0593R6 并被接受为针对自 C++98 以来所有 C++ 版本的 DR,然后添加到 C++20 规范中,新措辞:

[intro.object]

  1. The constructs in a C++ program create, destroy, refer to, access, and manipulate objects. An object is created by a definition, by a new-expression, by an operation that implicitly creates objects (see below)...

...

  1. Further, after implicitly creating objects within a specified region of storage, some operations are described as producing a pointer to a suitable created object. These operations select one of the implicitly-created objects whose address is the address of the start of the region of storage, and produce a pointer value that points to that object, if that value would result in the program having defined behavior. If no such pointer value would give the program defined behavior, the behavior of the program is undefined. If multiple such pointer values would give the program defined behavior, it is unspecified which such pointer value is produced.

C++20 规范中给出的 example 是:

#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;
}