为什么在堆数组初始化中调用了两次复制构造函数?

对于以下 C++14 代码,为什么 g++ 生成的代码new A[1]{x}似乎调用了复制构造函数两次?

#include <iostream>
using namespace std;

class A {
public:
    A()           { cout << "default ctor" << endl; }
    A(const A& o) { cout << "copy ctor" << endl;    }
    ~A()          { cout << "dtor" << endl;         }
};

int main()
{
    A x;
    cout << "=========" << endl;
    A* y = new A[1]{x};
    cout << "=========" << endl;
    delete[] y;
    return 0;
}

编译和输出:

$ g++ -fno-elide-constructors -std=c++14 test.cpp && ./a.out
default ctor
=========
copy ctor
copy ctor
dtor
=========
dtor
dtor

有趣的是,对于相同的代码,clang++ 只调用一次复制构造函数:

$ clang++ -fno-elide-constructors -std=c++14 test.cpp && ./a.out
default ctor
=========
copy ctor
=========
dtor
dtor

此外,在使用 g++ 时,将该A* y = new A[1]{x};行更改为以下任何一项都会导致复制构造函数仅被调用一次:

  • A* y = new A {x}; - 普通堆对象而不是大小为 1 的堆数组
  • A y[1] {x}; - 堆栈上的数组而不是堆

所以看起来双拷贝构造函数行为只在堆数组初始化中表现出来。

回答

TL;DR:这可能是 GCC 缺陷,{x}在这种情况下被误解为暂时的。对于 中的每个元素new A[N]{x1, x2, ... xN},复制构造函数应该根据[decl.init]和被调用一次[new.expr]。相反,GCC可能将其解释为初始化列表,因此部分解释为中间右值。不过,我们可以强制 GCC 以其他方式解释它。


为什么 g++ 生成的代码new A[1]{x}似乎两次调用复制构造函数?

因为没有移动构造函数。如果我们添加一个移动构造函数和更多输出,我们可以更好地了解情况(编译器资源管理器):

#include <iostream>
using namespace std;

class A {
public:
    A()           { cout << "default ctor @" << this << endl; }
    A(A&& o)      { cout << "move ctor: " << &o << " to " << this << endl;    }
    A(const A& o) { cout << "copy ctor: " << &o << " to " << this << endl;    }
    ~A()          { cout << "dtor @" << this << endl;         }
};

int main()
{
    A x;
    cout << "=========" << endl;
    A* y = new A[1]{x};
    cout << "=========" << endl;
    delete[] y;
    return 0;
}

请注意,我们新A(A&&)构造函数的存在向我们展示了中间临时:

default ctor @0x7ffec28b5476
=========
copy ctor: 0x7ffec28b5476 to 0x7ffec28b5477
move ctor: 0x7ffec28b5477 to 0x55d0a7fa6288
dtor @0x7ffec28b5477
=========
dtor @0x55d0a7fa6288
dtor @0x7ffec28b5476

事实上,如果我们A(A&&) = delete构造函数,g++甚至不会再编译它(但 Clang 仍然接受它)。

似乎 g++ 误解了支撑初始化列表。恕我直言,[expr.new] 可能允许这种解释,但这似乎是一个 g++ 缺陷,可能应该这样报告。

然而,整个磨难让我想起了我的一个老问题(初始化时真的需要花括号吗?)。因此,让我们引入更多大括号以确保g++不会误解我们的初始化程序:

int main()
{
    A x;
    cout << "=========" << endl;
    A* y = new A[1]{{{x}}};
    cout << "=========" << endl;
    delete[] y;
    return 0;
}

这个变体规避了 g++ 的行为:

initializer for T[1]     start : {
initializer for first element  : {
actual initializer for A       : {x}
initializer for T[1]     start : {
initializer for first element  : {
actual initializer for A       : {x}

然后程序输出是 ( Explorer )

因此,对于多个元素,我们最终会陷入困境(编译器资源管理器):

default ctor @0x7ffede3d9967
=========
copy ctor: 0x7ffede3d9967 to 0x1eb0ec8
=========
dtor @0x1eb0ec8
dtor @0x7ffede3d9967

同样,没有调用额外的构造函数:

int main()
{
    A x;
    cout << "=========" << endl;
    A* y = new A[2]{{{x},{{x}}};
    cout << "=========" << endl;
    delete[] y;
    return 0;
}


以上是为什么在堆数组初始化中调用了两次复制构造函数?的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>