考虑以下示例代码:
class C
{
public:
int* x;
};
void f()
{
C* c = static_cast<C*>(malloc(sizeof(C)));
c->x = nullptr; // <-- here
}
如果由于任何原因,我不得不使用未初始化的内存(当然,如果可能的话,我会调用new C()
),我仍然可以调用放置构造函数。但是,如果我忽略了这一点,如上所述,并手动初始化每个成员变量,是否会导致未定义的行为?也就是说,绕过构造函数本身是未定义的行为,还是用类外的等效代码代替调用它是合法的?
(这是通过另一个关于完全不同的问题发现的;询问好奇心…)
它现在是合法的,并且从C++98开始追溯
事实上,在C++20之前,C++规范的措辞将对象定义为(例如,C++17的措辞,[interro.object]):
C++程序中的结构创建、销毁、引用、访问和操纵对象。对象是通过定义(6.1)创建的隐式更改活动成员时的新表达式(8.5.2.4)联合(12.3),或创建临时对象时(7.4、15.2)。
没有提到使用malloc分配创建对象的可能性。使其成为事实上的未定义行为。
当时人们认为这是一个问题,后来由https://wg21.link/P0593R6并被接受为自C++98以来所有C++版本的DR,然后添加到C++20规范中,使用新的措辞:
[interro.object]
- C++程序中的构造创建、销毁、引用、访问和操作对象。对象是由定义、新表达式、通过隐式创建对象的操作创建的(请参见下文)
。。。
- 此外,在存储,一些操作被描述为生成指向合适的创建对象。这些操作选择隐式创建的对象,其地址是起始地址存储区域的,并生成指向的指针值如果该值将导致程序定义行为如果没有这样的指针值将给出程序定义行为,程序的行为是未定义的。如果多次这样指针值将给出程序定义的行为,它是未指明产生哪一个这样的指针值
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;
}
没有活动的C
对象,所以假装有对象会导致未定义的行为。
委员会Oulu会议通过的P0137R1通过将对象定义如下([interro.object]/1)来明确这一点:
对象由定义([basic.def])、新表达式([expr.new])、隐式更改联合的活动成员([class.union])或创建临时对象([conv.rval]、[class.temporary])创建。
reinterpret_cast<C*>(malloc(sizeof(C)))
不是这些。
另请参阅这个std建议线程,其中有一个来自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引号仅适用于首先创建对象的情况。请注意,"琐碎"或"空洞"(在CWG1751完成术语更改后)初始化,如[basic.life]/1中使用的术语,是对象的属性,而不是类型,因此"存在对象,因为它的初始化是空洞/琐碎的"是向后的。
我认为代码是可以的,只要类型有一个琐碎的构造函数,就像你的一样。在不调用放置new
的情况下使用从malloc
转换的对象只是在调用其构造函数之前使用对象。来自C++标准12.7〔class.dctor〕:
对于具有非平凡构造函数的对象,在构造函数开始执行之前引用该对象的任何非静态成员或基类会导致未定义的行为。
由于异常证明了规则,在构造函数开始执行之前,引用具有琐碎构造函数的对象的非静态成员不是UB。
在同一段落中有一个例子:
extern X xobj;
int* p = &xobj.i;
X xobj;
当X
是非平凡时,此代码被标记为UB,但当X
是平凡时,则不标记为UB。
在大多数情况下,绕过构造函数通常会导致未定义的行为。
可以说,对于普通的旧数据类型,存在一些的角点情况,但无论如何,首先要避免它们,构造函数是微不足道的。代码是否如所示简单?
[基本寿命]/1
对象或引用的生存期是该对象或引用在运行时的属性。如果一个对象是类或聚合类型,并且它或它的一个子对象是由除平凡默认构造函数之外的构造函数初始化的,则称它具有非空初始化。[注意:由一个琐碎的复制/移动构造函数初始化是非空洞初始化。--结束注释]T类型对象的生存期从以下时间开始:
获得了T型具有适当排列和大小的
- 存储,并且
- 如果对象具有非真空初始化,则其初始化完成
类型T的对象的生存期在以下情况下结束:
- 如果T是具有非平凡析构函数([class.dtor])的类类型,则析构函数调用启动,或者
- 对象占用的存储器被重新使用或释放
除了代码更难阅读和推理之外,你要么不会赢得任何东西,要么会遇到未定义的行为只要使用构造函数,它就是惯用的C++
这个特定的代码很好,因为C
是POD。只要C
是POD,它也可以用这种方式初始化。
你的代码相当于:
struct C
{
int *x;
};
C* c = (C*)malloc(sizeof(C));
c->x = NULL;
它看起来不熟悉吗?一切都很好。此代码没有问题。
虽然可以用这种方式初始化所有显式成员,但不能初始化类可能包含的所有内容:
-
引用不能在初始值设定项列表之外设置
-
vtable指针根本不能被代码操纵
也就是说,当你有一个虚拟成员、虚拟基类或引用成员时,除了调用它的构造函数之外,没有办法正确初始化你的对象。
我认为它不应该是UB。你让你的指针指向一些原始内存,并以特定的方式处理它的数据,这里没有什么不好的。
如果这个类的构造函数做了一些事情(初始化变量等),你会再次得到一个指向原始、未初始化对象的指针,在不知道(默认)构造函数应该做什么(并重复其行为)的情况下,使用它将是UB。