何时为此结构调用复制构造函数?

时间:2015-06-20 17:46:09

标签: c++

我正在尝试使用{}列表进行一些测试。当我在VS2015中编译它时,输出是

copy A 0

只是不明白,复制构造函数在哪里?

#include <iostream>

struct A
{
    A() = default;
    A(int i) : m_i(i) {}
    A(const A& a)
    {
        std::cout << "copy A " << m_i << std::endl;
    }
    int m_i = 0;
};

struct B
{
    B(const A& a) : m_a(a) {}
    B(const A& a1, const A& a2) {}
    A m_a;
};

int main()
{
    A a1{1}, a2{2};
    B b({ a1, a2 });
    return 0;
}

1 个答案:

答案 0 :(得分:15)

简短版本:

B b({a1, a2})之类的直接初始化中, braced-init-list {a1, a2}被视为{的一个参数{ {1}}。此参数B将用于初始化构造函数的第一个参数。 {a1, a2}包含隐式声明的构造函数B。通过创建临时B(B const&) ,可以从B const&初始化参考{a1, a2}。此临时对象包含B子对象,此子对象最终将通过A复制构造函数复制到b.m_a

比较:

B(B const&)

我们无法复制表单void foo(B const& b0); foo({a1, a2}); // one argument, creates a temporary `B` B b{a1, a2}B b(a1, a2)的初始化内容,因为这些情况会考虑B b = {a1, a2}a1作为(单独的)参数 - 除非存在可行的a2构造函数。

长版:

std::initializer_list包含以下构造函数:

B
由于缺乏对隐式提供的特殊移动功能的支持,

B(const A& a) : m_a(a) {} // #0 B(const A& a1, const A& a2) {} // #1 B(B const&) = default; // implicitly declared #2 B(B&&) = default; // implicitly declared #3 将不会出现在VS2013中。 OP的程序中没有使用#0。

初始化#3必须选择其中一个构造函数。我们只提供一个参数B b({a1, a2}),因此#1不可行。 #0也不可行,因为{a1, a2}不能从两个参数构造。 #2和#3都仍然可行(#3在VS2013中不存在)。

现在,重载决策尝试从A初始化B const&B&&。将创建临时{a1, a2}并将其绑定到此引用。如果#3存在,则过载分辨率将优先选择#3至#2。

临时创建再次查看上面显示的四个构造函数,但现在我们有两个参数Ba1(或a2,但这与此无关)。 #1是唯一可行的重载,临时是通过initializer_list创建的。

因此,我们基本上以B(const A& a1, const A& a2)为基础。从临时B b( B{a1, a2} )B{a1, a2}的复制(或移动)可以省略(copy-elision)。这就是为什么g ++和clang ++不会调用副本ctor,也不会调用bB的移动ctor。

VS2013似乎没有在这里删除复制构造,并且它不能移动构造,因为它不能隐含地提供#3(VS2015将修复它)。因此,VS2013会调用A,将B(B const&)复制到B{a1, a2}.m_a。这会调用b.m_a的复制构造函数。

如果#3存在,并且移动未被删除,则调用隐式声明的移动构造函数#3。由于A具有显式声明的复制构造函数,因此不会为A隐式声明移动构造函数。这也导致从AB{a1, a2}.m_a的复制构造,但是通过b.m_a的移动ctor。

在VS2013中,如果我们手动将移动广告添加到BA,我们会注意到B将被移动而不是复制:

A

通过跟踪每个构造函数来理解此类程序通常更容易。使用特定于MSVC的#include <iostream> #include <utility> struct A { A() = default; A(int i) : m_i(i) {} A(const A& a) { std::cout << "copy A " << m_i << std::endl; } A(A&& a) { std::cout << "move A " << m_i << std::endl; } int m_i = 0; }; struct B { //B(const A& a) : m_a(a) {} B(const A& a1, const A& a2) {} B(B const&) = default; B(B&& b) : m_a(std::move(b.m_a)) {} A m_a; }; (g ++ / clang ++可以使用__FUNCSIG__):

__PRETTY_FUNCTION__

打印(没有评论):

__thiscall A::A(int)  // a1{1}
__thiscall A::A(int)  // a2{2}
__thiscall A::A(void) // B{a1, a2}.m_a, default-constructed
__thiscall B::B(const struct A &,const struct A &) // B{a1, a2}
__thiscall A::A(const struct A &) // b.m_a(B{a1, a2}.m_a)
__thiscall B::B(const struct B &) // b(B{a1, a2})

其他事实:

  • VS2015和VS2013 执行都忽略#include <iostream> #define PRINT_FUNCSIG() { std::cout << __FUNCSIG__ << "\n"; } struct A { A() PRINT_FUNCSIG() A(int i) : m_i(i) PRINT_FUNCSIG() A(const A& a) : m_i(a.m_i) PRINT_FUNCSIG() int m_i = 0; }; struct B { B(const A& a1, const A& a2) PRINT_FUNCSIG() B(B const& b) : m_a(b.m_a) PRINT_FUNCSIG() A m_a; }; int main() { A a1{1}, a2{2}; B b({ a1, a2 }); return 0; } 的副本构建,但不包括原始B b(B{a1, a2});的副本构建。