绕过一个类的构造函数是合法的还是会导致未定义的行为?

Aco*_*gua 15 c++ malloc object-lifetime language-lawyer

请考虑以下示例代码:

class C
{
public:
    int* x;
};

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

如果由于任何原因我不得不忍受未初始化的内存(当然,如果可能的话,我会打电话new C()),我仍然可以调用放置构造函数.但是,如果我省略这一点,如上所述,并手动初始化每个成员变量,是否会导致未定义的行为?即绕过构造函数本身未定义的行为,或者用类外的一些等效代码替换调用它是否合法?

(通过另一个完全不同的问题遇到这个问题;要求好奇......)

Ami*_*rsh 10

它现在是合法的,并且从 C++98 开始追溯!

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

C++ 程序中的构造创建、销毁、引用、访问和操作对象。对象由定义 (6.1)、new 表达式 (8.5.2.4)、隐式更改联合的活动成员 (12.3) 或创建临时对象 (7.4, 15.2) 创建。

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

然后它被视为一个问题,这个问题后来由https://wg21.link/P0593R6解决,并被接受为自 C++98 以来所有 C++ 版本的 DR,然后添加到 C++20 规范中,新的措辞:

[介绍对象]

  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;
}
Run Code Online (Sandbox Code Playgroud)

  • 万岁,这么多年过去了,我的代码突然开始以定义的方式运行(: (2认同)
  • @GabrielMorin 正式在当前版本上发布了 DR。在本例中为 C++17。然而,在某些情况下,DR 指向标准中缺失的某个 _lacuna_ ,但实际上_所有编译器都以某种方式处理它(例如,没有其他合理的方法来解释它)。对于我们的例子,如果没有新的措辞,理论上`std::vector`(以及 C++17 `std::Optional`)的实现中就会有一个 UB。由于我们假设“std::vector”中没有 UB,因此修复它的措辞必须被认为与 C++98 相关。这就是人们的看法。 (2认同)

T.C*_*.C. 6

没有生物C,所以假装有一个会导致未定义的行为.

在委员会的奥卢会议上通过的P0137R1通过如下定义对象来明确这一点([intro.object]/1):

一个目的是通过一个定义创建([basic.def]),通过一个新的表达式([expr.new])中,当隐式地改变联合的活性件([class.union]),或者当一个临时对象创建([conv.rval],[class.temporary]).

reinterpret_cast<C*>(malloc(sizeof(C))) 不是这些.

另请参阅此std-proposal主题,其中一个非常类似的例子来自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
Run Code Online (Sandbox Code Playgroud)

[basic.life]/1引用仅在首先创建对象时适用.请注意,"简单"或"空白"(在CWG1751完成术语更改之后)初始化,因为该术语在[basic.life]/1中使用,是对象的属性,而不是类型,因此"有一个对象,因为它的初始化是空的/平凡的"是倒退的.

  • 那么这也适用于`int`吗?如果没有,为什么不呢?如果是的话,我们可以"malloc"吗? (6认同)
  • 还要考虑这个脚注"39)本节不对通过指向未被`:: operator new`分配的内存的指针施加间接限制.这保持了许多C++实现使用二进制库和用其他语言编写的组件的能力. ,这适用于C二进制文件,因为通过指向std :: malloc分配的内存的指向的间接不受限制." (5认同)
  • @hvd一个新的位置(因为这是在这里绕过的)有两个效果:1.它创建一个对象; 2.它运行初始化.仅仅因为#2没有效果并不意味着你可以绕过#1. (4认同)
  • `C`是POD.如果是这样,那么我不会看到*为什么*应该是UB. (3认同)
  • 至少这是http://en.cppreference.com/w/cpp/concept/PODType所说的(你可以通过malloc创建POD).我目前在标准中找不到确凿的证据,但如果对齐请求,它允许将`T*`转换为'U*`.`U`并不比`T`更严格,如果两者都是标准布局类型.(其中也没有任何实际的`U`对象,通过定义,新表达或实现创建.) (3认同)
  • @TC:你的整个答案都依赖于`[intro.object]`中的一段摘录,并且当它不是时,它被认为是一个详尽的列表.相同段落的开头:**对象是存储区域.**.`malloc`肯定会分配一个对象. (3认同)
  • @TC:同样"A,B和C创建对象"并不意味着"A,B和C是创建对象的唯一方法". (3认同)
  • @TC:这使它与C不兼容? (2认同)
  • @BenVoigt当然,但你说的另一种方式是什么?标准说A,B和C可以创建一个对象; 它没有说任何其他东西创造一个对象. (2认同)