将一个C++对象传递到自己的构造函数中是否合法?

114

我很惊讶地发现以下内容居然可以正常运行:

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

我将构造对象的地址传递到它自己的构造函数中。从源代码层面看,这似乎是一个循环定义。标准真的允许在对象构造之前将对象传递给函数吗?还是这是未定义的行为?
我想,这并不奇怪,因为所有类成员函数已经有一个指向其类实例数据的隐式参数。而且数据成员的布局在编译时是固定的。
请注意,我不是在问这是否有用或者是一个好主意;我只是在摸索学习更多关于类的知识。

1
@ShafikYaghmour 你为什么删除了你的回答?只需添加 [basic.life]p6 的引用以获取限制信息即可。 - dyp
2
没问题,这基本上与在构造函数中使用this一样,具有所有的缺陷。 - Kerrek SB
2
这不更像是 size_t x = sizeof(x) 吗?对象的构造函数在分配内存时被调用(来自未指定的源)。只要您仅依赖于存储的属性,而不依赖于任何值的解释,事情就应该是安全的。 - MSalters
1
Shafik,按照你的建议,我暂时取消了你的答案的采纳,尽管它已经对我足够深入了 :) - Andrew Wagner
1
我心中所想的例子是,一个实现可能会在初始化引用时检查它是否引用了已分配的存储空间(而不是未分配的存储空间)。这个特定的DS9K实现在调用构造函数之前刚刚分配了存储空间,因此我们得到一个序列:引用绑定->分配->构造函数调用。(或者,存储地址的计算可能会延迟到实际分配,因此引用绑定是不可能的。) - dyp
显示剩余12条评论
3个回答

68
这不是未定义行为。虽然foo未初始化,但您正在以标准允许的方式使用它。在为对象分配空间但尚未完全初始化之后,您可以有限地使用它。绑定对该变量的引用和获取其地址都是允许的。
这是由defect report 363: Initialization of class from self所涵盖的。

And if so, what is the semantics of the self-initialization of UDT? For example

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

can be compiled and prints:

A::A(const A&) 0253FDD8 0253FDD8
A::~A() 0253FDD8
and the resolution was:

3.8 [basic.life] paragraph 6 indicates that the references here are valid. It's permitted to take the address of a class object before it is fully initialized, and it's permitted to pass it as an argument to a reference parameter as long as the reference can bind directly. Except for the failure to cast the pointers to void * for the %p in the printfs, these examples are standard-conforming.

The full quote of section 3.8 [basic.life] from the draft C++14 standard is as follows:

类似地,在对象的生命周期开始之前,但在分配了对象将要占用的存储空间之后,或者在对象的生命周期结束之后,在重新使用或释放了对象所占用的存储空间之前,任何引用原始对象的glvalue都可以使用,但仅有限制性地。有关正在构建或销毁的对象,请参见12.7。否则,这样的glvalue是指向已分配存储空间(3.7.4.2)的,并且使用不依赖于其值的glvalue属性是定义良好的。如果程序满足以下条件,则具有未定义行为: - 对此类glvalue应用lvalue-to-rvalue转换(4.1); - 使用glvalue访问非静态数据成员或调用对象的非静态成员函数; - 将glvalue绑定到虚基类的引用(8.5.3); - 将glvalue用作dynamic_cast(5.2.7)的操作数或typeid的操作数。

我们没有做任何与上述内容定义的未定义行为相关的事情,与foo无关。

如果我们尝试在Clang中执行此操作,我们会看到一个不祥的警告(请现场查看):

警告:当在其自身初始化期间使用未初始化的变量'foo'时[-Wuninitialized]

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

int x = x ;

does invoke undefined behavior.

活跃问题453:引用只能绑定到“有效”的对象似乎也相关,但仍未解决。最初提出的语言与缺陷报告363一致。


啊,好的 :) (不知怎么的,我没有考虑到这一点,而是把它视为理所当然。)尽管如此,我认为标准中还缺少一个小细节。 - dyp
@dyp:确实没有明确定义,但它必须在第一个基类构造函数开始执行之前被排序。你只是无法确定提前多少时间。 - MSalters
3
谢谢Shafik!Stack Overflow上的人们以及该网站将正确的问题放在正确的人眼前的能力,令我惊叹不已! - Andrew Wagner
抱歉,我在谈论原帖/问题。不管怎样,重新考虑一下,我可能误解了“constructed”这部分。我以为OP的意思是我们将一个构造对象传递到该对象的构造函数中,而实际上我们将一个未初始化的对象传递到构造函数中。 - dyp

16
构造函数在分配对象所需的内存时被调用。此时,该位置上不存在任何对象(或可能存在一个具有平凡析构函数的对象)。此外,this指针指向该内存并且内存已适当地对齐。
由于这是分配和对齐的内存,我们可以使用Foo类型的lvalue表达式(即Foo&)来引用它。但是,在进入构造函数体之前,我们还不能使用lvalue-to-rvalue转换。
在这种情况下,代码仅尝试在构造函数体内打印&bar。甚至可以在此处打印bar.member。因为构造函数体已经被执行,Foo对象已经存在,可以读取其成员。
这留下了一个小细节,那就是名称查找。在Foo foo(foo)中,第一个foo引入了作用域中的名称,因此第二个foo引用了刚声明的名称。这就是为什么int x = x无效,但int x = sizeof(x)有效的原因。

但是我使用g++ 4.8编译int x = x时没有收到任何错误。无效的语句应该会报错,对吧? - cbinder
@cbinder:_应该_,但不幸的是 _不会_。 - MSalters

0

正如其他答案所述,只要在使用其值之前对其进行初始化,对象就可以使用自身进行初始化。您仍然可以将对象绑定到引用或获取其地址。 但是除了它是有效的事实之外,让我们探讨一个使用示例。

下面的示例可能会引起争议,您肯定可以提出许多其他实现想法。然而,它展示了这个奇怪的C++属性的有效用法,即您可以将一个对象传递到其自身的构造函数中。

class Employee {
   string name;
   // manager may change so we don't hold it as a reference
   const Employee* pManager; 
public:
  // we prefer to get the manager as a reference and not as a pointer
  Employee(std::string name, const Employee& manager)
    : name(std::move(name)), pManager(&manager) {}

  void modifyManager(const Employee& manager) {
      // TODO: check for recursive connection and throw an exception
      pManager = &manager;
  }

  friend std::ostream& operator<<(std::ostream& out, const Employee& e) {
      out << e.name << " reporting to: ";
      if(e.pManager == &e)
        out << "self";
      else
        out << *e.pManager;
      return out;
  }
};

现在我们来讲解如何使用对象对自身进行初始化:

// it is valid to create an employee who manages itself
Employee jane("Jane", jane);

事实上,根据类 Employee 的给定实现,用户别无选择,只能将第一个创建的员工初始化为其自身的经理,因为还没有其他员工可以传递。从某种意义上讲,这是有道理的,因为第一个创建的员工应该管理自己。
代码:http://coliru.stacked-crooked.com/a/9c397bce622eeacd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接