将C++对象传递给自己的构造函数是合法的吗?

And*_*ner 109 c++ constructor class undefined-behavior language-lawyer

我很惊讶地意外地发现以下工作:

#include <iostream>            
int main(int argc, char** argv)
{
  struct Foo {
    Foo(Foo& bar) {
      std::cout << &bar << std::endl;
    }
  };
  Foo foo(foo); // I can't believe this works...
  std::cout << &foo << std::endl; // but it does...
}
Run Code Online (Sandbox Code Playgroud)

我将构造对象的地址传递给它自己的构造函数.这看起来像源级别的循环定义.标准是否真的允许您在构造对象之前将对象传递给函数,还是这种未定义的行为?

鉴于所有类成员函数已经将指向其类实例的数据的指针作为隐式参数,我认为这并不奇怪.并且数据成员的布局在编译时是固定的.

请注意,我不是在问这是有用还是好主意; 我只是在修补一些关于课程的更多信息.

Sha*_*our 65

这不是未定义的行为.虽然foo未初始化,但您使用的方式是标准允许的方式.为对象分配空间但在完全初始化之前,您可以使用有限的方式.允许绑定对该变量的引用并获取其地址.

这包括缺陷报告363:来自self的类的初始化,其中说:

如果是这样,UDT的自我初始化的语义是什么?例如

 #include <stdio.h>

 struct A {
        A()           { printf("A::A() %p\n",            this);     }
        A(const A& a) { printf("A::A(const A&) %p %p\n", this, &a); }
        ~A()          { printf("A::~A() %p\n",           this);     }
 };

 int main()
 {
  A a=a;
 }
Run Code Online (Sandbox Code Playgroud)

可以编译和打印:

A::A(const A&) 0253FDD8 0253FDD8
A::~A() 0253FDD8
Run Code Online (Sandbox Code Playgroud)

决议是:

3.8 [basic.life]第6段表明此处的引用是有效的.允许在完全初始化之前获取类对象的地址,并且只要引用可以直接绑定,就允许将它作为参数传递给引用参数.除了没有为printfs中的%p转换指针void*之外,这些示例符合标准.

C++ 14标准草案中3.8 [basic.life]部分的完整引用如下:

类似地,在对象的生命周期开始之前但在对象将占用的存储已经被分配之后,或者在对象的生命周期结束之后并且在对象占用的存储被重用或释放之前,任何引用的glvalue之前可以使用原始对象,但仅限于有限的方式.对于正在建造或销毁的物体,见12.7.否则,这样的glvalue指的是已分配的存储(3.7.4.2),并且使用不依赖于其值的glvalue的属性是明确定义的.如果出现以下情况,该程

  • 左值到右值的转换(4.1)应用于这样的glvalue,

  • glvalue用于访问非静态数据成员或调用对象的非静态成员函数,或

  • glvalue绑定到对虚基类(8.5.3)的引用,或

  • glvalue用作dynamic_cast(5.2.7)的操作数或typeid的操作数.

我们没有采取任何措施,foo因为它们属于上述子弹所定义的未定义行为.

如果我们和Clang一起尝试这个,我们会看到一个不祥的警告(现场观看):

警告:变量'foo'在自己的初始化中使用时未初始化[-Wuninitialized]

这是一个有效的警告,因为从未初始化的自动变量生成不确定的值是未定义的行为.但是,在这种情况下,您只是绑定一个引用并在构造函数中获取变量的地址,该地址不会产生不确定的值且有效.另一方面,C++ 11标准草案中以下自初始化示例如下:

int x = x ;
Run Code Online (Sandbox Code Playgroud)

会调用未定义的行为.

活动问题453:引用可能仅绑定到"有效"对象似乎也相关但仍处于打开状态.最初提出的语言与缺陷报告363一致.

  • 谢谢沙菲克!在Stack Overflow上闲逛的人,以及网站将正确的问题放在右眼前面的能力永远不会让我感到惊讶! (3认同)

MSa*_*ers 15

在为将要分配的内存分配内存的位置调用构造函数.此时,该位置不存在任何对象(或者可能是具有普通析构函数的对象).此外,this指针指的是该存储器和存储器正确对齐.

由于它是已分配和对齐的内存,我们可以使用Foo类型(即Foo&)的左值表达式来引用它.我们可以还做的是有一个左到右值的转换.只有在输入构造函数体之后才允许这样做.

在这种情况下,代码只是尝试&bar在构造函数体内打印.bar.member在这里打印甚至是合法的.由于已输入构造函数体,因此Foo存在对象并且可以读取其成员.

这给我们留下了一个小细节,那就是名字查找.在Foo foo(foo),第一个foo引入范围中的名称,第二个foo引用返回刚刚声明的名称.这int x = x就是无效的原因,但是int x = sizeof(x)有效.