【问题标题】:Is passing a C++ object into its own constructor legal?将 C++ 对象传递给它自己的构造函数是否合法?
【发布时间】:2015-12-13 00:45:44
【问题描述】:

我惊讶地发现以下工作:

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

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

我想这并不奇怪,因为所有类成员函数都已经有一个指向它们的类实例的数据的指针作为隐式参数。并且数据成员的布局在编译时是固定的。

注意,我不是在问这是有用还是好主意;我只是在修补以了解有关课程的更多信息。

【问题讨论】:

  • @ShafikYaghmour 你为什么删除你的答案?只需添加 [basic.life]p6 的引用即可。
  • 是的,没关系;它本质上与在构造函数中使用 this 相同,但存在所有缺陷。
  • 这不是更像size_t x = sizeof(x) 吗?对象的构造函数在分配内存的点调用(来自未指定的源)。只要您只依赖于存储的属性,而不依赖于任何值解释,事情就应该是安全的。
  • @MSalters 关于存储是否分配在将对象绑定到引用之前,也就是在构造函数调用之前,存在一个微妙的问题。
  • Shafik,在你的建议下,我暂时不接受你的回答,虽然它对我来说已经很深了:)

标签: c++ class constructor language-lawyer undefined-behavior


【解决方案1】:

这不是未定义的行为。尽管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] 部分的完整引用如下:

类似地,在对象的生命周期开始之前但在对象的生命周期开始之后 对象将占用的存储空间已被分配,或者在 对象的生命周期已经结束并且在存储之前 被占用的对象被重用或释放,任何引用 可以使用原始对象,但只能以有限的方式使用。对于一个对象 正在建造或毁坏,见 12.7。否则,这样的glvalue 指分配的存储(3.7.4.2),并使用 不依赖于其值的 glvalue 是明确定义的。该程序 在以下情况下具有未定义的行为:

  • 将左值到右值的转换 (4.1) 应用于此类左值,

  • 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 一致。

【讨论】:

  • 啊,好吧 :)(不知何故,我没有考虑这一点,并认为这是理所当然的。)不过,我认为标准中缺少一个小细节。
  • @dyp:确实没有精确指定,但必须在第一个基类构造函数开始执行之前排序。你只是无法确定之前有多少。
  • 谢谢沙菲克!在 Stack Overflow 上闲逛的人,以及该网站将正确的问题摆在正确的眼前的能力永远让我感到惊讶!
  • 对不起,我在谈论原始帖子/问题。不管怎样,再想一想,我可能误解了“构造”部分。我认为 OP 意味着我们将 constructed object 传递给该对象的构造函数,而实际上我们将 uninitialized object 传递给构造函数。
【解决方案2】:

在为对象分配内存的位置调用构造函数。此时,该位置不存在任何对象(或者可能是具有微不足道的析构函数的对象)。此外,this 指针指向该内存并且内存已正确对齐。

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

在这种情况下,代码只是尝试在构造函数主体内打印&amp;bar。在这里打印bar.member 甚至是合法的。由于进入了构造函数体,所以存在Foo对象,可以读取其成员。

这给我们留下了一个小细节,那就是名称查找。在Foo foo(foo) 中,第一个foo 在范围内引入名称,第二个foo 因此引用刚刚声明的名称。这就是为什么int x = x 无效,而int x = sizeof(x) 有效。

【讨论】:

  • 但我在使用 g++ 4.8 编译 int x = x 时没有收到任何错误。无效的语句应该吐错误吧?
  • @cbinder:应该,但不幸的是不会
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-06-04
  • 2016-10-12
  • 2020-07-22
  • 2019-08-07
  • 2016-02-15
相关资源
最近更新 更多