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

Con*_* Ma 13 c++

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

copy A 0
Run Code Online (Sandbox Code Playgroud)

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

#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;
}
Run Code Online (Sandbox Code Playgroud)

dyp*_*dyp 15

精简版:

在直接初始化中B b({a1, a2}),braced-init-list {a1, a2}被认为是构造函数的一个参数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`
Run Code Online (Sandbox Code Playgroud)

我们不会看到任何复制表单的初始化,B b{a1, a2}B b(a1, a2)不会B b = {a1, a2},因为这些情况考虑a1a2作为(单独的)参数 - 除非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
Run Code Online (Sandbox Code Playgroud)

#3由于缺乏对隐式提供的特殊移动功能的支持,VS2013中不会出现这种情况.OP的程序中没有使用#0.

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

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

临时的创建再次查看上面显示的四个构造函数,但现在我们有两个参数a1a2(或者一个initializer_list,但这里不相关).#1是唯一可行的重载,临时是通过创建的B(const A& a1, const A& a2).

所以,我们基本上最终会结束B b( B{a1, a2} ).从临时B{a1, a2}到复制(或移动)b可以省略(复制 - 省略).这就是为什么g ++以及铛++不调用拷贝构造函数,也不既不移动构造函数B,也没有A.

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

如果#3存在,并且移动未被删除,则调用隐式声明的移动构造函数#3.由于A具有显式声明的复制构造函数,因此不会隐式声明任何移动构造函数A.这也导致了一份从建设B{a1, a2}.m_ab.m_a,但通过的移动构造函数B.


在VS2013,如果我们手动添加一个移动构造函数来AB,我们会发现,A将被移动而不是复制:

#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;
};
Run Code Online (Sandbox Code Playgroud)

通过跟踪每个构造函数来理解这些程序通常更容易.使用MSVC特定的__FUNCSIG__(g ++/clang ++可以使用__PRETTY_FUNCTION__):

#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;
}
Run Code Online (Sandbox Code Playgroud)

这打印(没有评论):

__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 都不会复制B b(B{a1, a2});原件的复制结构B b({a1, a2}).