这个结构体的拷贝构造函数会在什么时候被调用?

17
我正在尝试使用{}列表进行一些测试。当我在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
我得到了没有输出 - emlai
8
B b({a1, a2}); 几乎等同于 B b(B{a1, a2});。复制可能发生在从 B{a1, a2}.m_a 临时对象到 b.m_a 的过程中。 - dyp
@dyp 这可能是正确的 - 这似乎也是正确的行为。难道gcc/clang不应该捕捉到那个构造函数吗? - Barry
1
可以在这里查看行为。 - vsoftco
1
在使用g++编译时,我遇到了一些编译错误,纠正后,我没有看到任何输出。 - Rndp13
显示剩余5条评论
1个回答

19

简短版:

在直接初始化中,例如 B b({a1, a2}),花括号包围的初始化列表 {a1, a2} 被视为构造函数 B 的一个参数。这个参数 {a1, a2} 将被用于初始化构造函数的第一个参数。 B 包含一个隐式声明的构造函数 B(B const&)。通过创建一个临时的 B,可以从 {a1, a2} 初始化引用 B const&。这个临时对象包含一个 A 子对象,并且这个子对象最终将通过 B(B const&) 拷贝构造函数被复制到 b.m_a 中。

相比之下:

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} ,除非存在可行的 std::initializer_list 构造函数,否则我们将不会看到任何复制,因为这些情况将 a1a2 视为(分开的)参数。

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

#3由于缺乏隐式提供特殊移动函数的支持,不会出现在VS2013中。 #0在OP的程序中未使用。

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

现在,重载决议尝试从{a1,a2}初始化B const&B&&。将创建一个临时的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++和clang++不会调用BA的复制构造函数或移动构造函数。

在这里,VS2013似乎无法省略复制构造。它也不能移动构造,因为它不能隐式提供#3(VS2015将修复该问题)。因此,VS2013调用B(B const&),它将B {a1,a2}.m_a复制到b.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;
};

跟踪每个构造函数通常更容易理解这样的程序。使用 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;
}

这将打印(不包括注释):

__thiscall A::A(int)  // a1{1}
__thiscall A::A(int)  // a2{2}
__thiscall A::A(void) // B{a1, a2}.m_a,默认构造
__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})

你可以看到,由于A的移动构造函数被删除了,因为显式复制操作禁用了隐式移动,所以B的隐式移动构造函数调用了A的复制构造函数。 - vsoftco
@vsoftco 在VS2013中,BA都没有移动构造函数。但是你对标准是正确的。 - dyp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接