我很惊讶地意外发现以下情况:
#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...
}
我将构造对象的地址传递给它自己的构造函数。这看起来像源级别的循环定义。标准是否真的允许您在构造对象之前将对象传递给函数,还是这种未定义的行为?
我认为,鉴于所有类成员函数已经将指向其类实例的数据的指针作为隐式参数,这并不奇怪。并且数据成员的布局在编译时是固定的。
注意,我不会问这是有用还是好主意;我只是在修补一些关于课程的知识。
答案 0 :(得分:65)
这不是未定义的行为。虽然foo
未初始化,但您使用的方式是标准允许的方式。为对象分配空间但在完全初始化之前,您可以使用有限的方式。允许绑定对该变量的引用并获取其地址。
这由defect report 363: Initialization of class from 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; }
可以编译和打印:
A::A(const A&) 0253FDD8 0253FDD8 A::~A() 0253FDD8
,决议是:
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一起尝试,我们会看到一个不祥的警告( see it live ):
警告:变量'foo'在其自己的初始化中使用时未初始化[-Wuninitialized]
这是producing an indeterminate value from an uninitialized automatic variable is undefined behavior以来的有效警告。但是,在这种情况下,您只是绑定一个引用并在构造函数中获取变量的地址,该地址不会产生不确定的值且有效。另一方面,following self-initialization example from the draft C++11 standard:
int x = x ;
会调用未定义的行为。
Active issue 453: References may only bind to “valid” objects 似乎也很重要但仍然开放。最初提出的语言与缺陷报告363一致。
答案 1 :(得分:15)
在为将要分配内存的位置调用构造函数。此时,该位置不存在任何对象(或者可能是具有普通析构函数的对象)。此外,this
指针指的是内存并且内存已正确对齐。
由于它是已分配和对齐的内存,我们可以使用Foo
类型的左值表达式(即Foo&
)来引用它。我们不的做法是进行左值到右值的转换。只有在输入构造函数体之后才允许这样做。
在这种情况下,代码只是尝试在构造函数体内打印&bar
。在这里打印bar.member
甚至是合法的。由于已输入构造函数体,因此存在Foo
对象,并且可以读取其成员。
这给我们留下了一个小细节,以及这个名字查找。在Foo foo(foo)
中,第一个foo
在范围中引入了名称,第二个foo
因此引用了刚才声明的名称。这就是int x = x
无效的原因,但int x = sizeof(x)
有效。