Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When is the copy constructor called for this struct?

Tags:

c++

I'm trying to do some test with {}-lists. When I compiled this in VS2015, the output is

copy A 0

Just don't get it, where is copy constructor called ?

#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;
}
like image 346
Cong Ma Avatar asked Jun 20 '15 17:06

Cong Ma


1 Answers

Short version:

In a direct-initialization like B b({a1, a2}), the braced-init-list {a1, a2} is considered as one argument to a constructor of B. This argument {a1, a2} will be used to initialize the first parameter of the constructor. B contains an implicitly-declared constructor B(B const&). A reference B const& can be initialized from {a1, a2} by creating a temporary B. This temporary contains an A subobject, and this subobject will finally be copied to b.m_a via the B(B const&) copy constructor.

Compare to:

void foo(B const& b0);
foo({a1, a2}); // one argument, creates a temporary `B`

We won't see any copying for initializations of the form B b{a1, a2} nor B b(a1, a2) nor B b = {a1, a2}, because those cases consider a1 and a2 as the (separate) arguments - unless a viable std::initializer_list constructor exists.


Long version:

The class B contains the following constructors:

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 will not be present in VS2013, due to a lack of support of implicitly-provided special move functions. #0 is not used in the OP's program.

The initialization B b({a1, a2}) has to select one of those constructors. We supply only one argument {a1, a2}, so #1 is not viable. #0 isn't viable either, since A cannot be constructed from two arguments. Both #2 and #3 are still viable (#3 doesn't exist in VS2013).

Now, overload resolution tries to initialize a B const& or a B&& from {a1, a2}. A temporary B will be created and bound to this reference. Overload resolution will prefer #3 to #2, if #3 exists.

The creation of the temporary again looks at the four constructors shown above, but now we have two arguments a1 and a2 (or an initializer_list, but that's irrelevant here). #1 is the only viable overload, and the temporary is created via B(const A& a1, const A& a2).

So, we essentially end up with essentially B b( B{a1, a2} ). The copy (or move) from the temporary B{a1, a2} to b can be elided (copy-elision). This is why g++ and clang++ don't call the copy ctor nor the move ctor of neither B nor A.

VS2013 seems not to elide the copy-construction here, and it cannot move-construct since it cannot implicitly provide #3 (VS2015 will fix that). Therefore, VS2013 calls B(B const&), which copies B{a1, a2}.m_a to b.m_a. This calls A's copy constructor.

If #3 exists, and the move is not elided, the implicitly-declared move constructor #3 is called. Since A has an explicitly-declared copy constructor, no move constructor will be implicitly declared for A. This also leads to a copy construction from B{a1, a2}.m_a to b.m_a, but via the move ctor of B.


In VS2013, if we manually add a move ctor to A and to B, we'll notice that A will be moved instead of copied:

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

It is typically easier to understand such programs by tracing from every constructor. Using the MSVC-specific __FUNCSIG__ (g++/clang++ can use __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;
}

This prints (w/o the comments):

__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})

Additional factoids:

  • Both VS2015 and VS2013 do elide the copy construction of B b(B{a1, a2}); but not the original B b({a1, a2}).
like image 174
dyp Avatar answered Sep 23 '22 06:09

dyp