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;
}
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:
B b(B{a1, a2});
but not the original B b({a1, a2})
.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With