将一个c++对象传递给它自己的构造函数是否合法

Is passing a C++ object into its own constructor legal?

本文关键字:自己的 它自己 构造函数 是否 一个 对象 c++      更新时间:2023-10-16

我意外地发现下面的工作:

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

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

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

注意,我不是在问这是否有用或是个好主意;我只是修修补补,多了解一些课程知识。

这不是未定义行为。尽管foo是未初始化的,但您正在以标准允许的方式使用它。在为对象分配空间之后,但在对象完全初始化之前,允许以有限的方式使用它。既可以绑定对该变量的引用,也可以取其地址。

这被缺陷报告363所覆盖:从self初始化类,它说:

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

 #include <stdio.h>
 struct A {
        A()           { printf("A::A() %pn",            this);     }
        A(const A& a) { printf("A::A(const A&) %p %pn", this, &a); }
        ~A()          { printf("A::~A() %pn",           this);     }
 };
 int main()
 {
  A a=a;
 }

可以编译并输出:

A::A(const A&) 0253FDD8 0253FDD8
A::~A() 0253FDD8

,分辨率为:

(基本

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

部分3.8 [基本]的完整报价。来自c++ 14标准草案如下:

类似地,在对象的生命周期开始之前,但在对象将要占用的存储空间已经被分配,或者在对象的生命周期已经结束,并且在对象所使用的存储空间之前被占用的对象被重用或释放,任何指向原物可以使用,但使用方式有限。对于一个对象正在建造或破坏中,见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]

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

int x = x ;

调用未定义行为

活动问题453:引用可能只绑定到"有效"对象,似乎也相关,但仍然打开。最初建议的语言与缺陷报告363一致。

构造函数在为将来的对象分配内存的地方调用。此时,该位置不存在对象(或者可能存在带有普通析构函数的对象)。此外,this指针指向该内存,并且内存已正确对齐。

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

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

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

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

下面的例子可能是有争议的,你当然可以提出许多其他的想法来实现它。然而,它给出了这个奇怪的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类的给定实现,用户别无选择,只能初始化有史以来创建的第一个Employee,并将其自身作为自己的管理器,因为还没有其他Employee可以传递。在某种程度上,这是有道理的,因为第一个员工应该管理自己。

代码:http://coliru.stacked-crooked.com/a/9c397bce622eeacd

相关文章: