Reputation: 1502
I have a question about the following code. My compiler is MSVC++ 17 Visual studio version 15.3 with compiler option /std:c++14 (as opposed to /std:c++latest) running in release mode:
struct Bar
{
int a;
std::string b;
Bar() { std::cout << "default\n"; }
Bar(int a, const std::string& b) : a{ a }, b{ b } { std::cout << "direct\n"; }
Bar(int a, std::string&& b) : a{ a }, b{ std::move(b) } { std::cout << "direct move b\n"; }
Bar(const Bar& other) : a{ other.a }, b{ other.b } { std::cout << "const copy\n"; }
Bar(Bar&& other) : a{ std::move(other.a) }, b{ std::move(other.b) } { std::cout << "move\n"; }
Bar& operator=(const Bar& other)
{
a = other.a;
b = other.b;
std::cout << "const assign\n";
return *this;
}
Bar& operator=(Bar&& other)
{
a = std::move(other.a); //would this even be correct?
b = std::move(other.b);
std::cout << "move assign\n";
return *this;
}
};
std::tuple<Bar, Bar> foo()
{
std::string s = "dsdf";
return { { 1, s }, { 5, "asdf" } };
}
int main()
{
Bar a, b;
std::tie(a, b) = foo();
std::cout << a.a << a.b << std::endl;
std::cout << b.a << b.b;
}
The output is:
default
default
direct
direct move b
const copy <-- Why copy? Why not move>
const copy <-- Why copy? Why not move>
move assign
move assign
1dsdf
5asdf
If I change return { { 1, s }, { 5, "asdf" } };
to return { Bar{ 1, s }, Bar{ 5, "asdf" } };
the output changes to:
default
default
direct
direct move b
move
move
move assign
move assign
1dsdf
5asdf
Question: Why isn't a move performed in both cases? Why is the copy constructor called in the first case?
Upvotes: 4
Views: 669
Reputation: 303127
The simplest distillation of your question is why:
std::tuple<Bar> t{{5, "asdf"}};
prints
direct move b
const copy
but
std::tuple<Bar> u{Bar{5, "asdf"}};
prints
direct move b
move
To answer that question, we have to determine what those two declarations actually do. And in order to do that, we have to understand which of std::tuple
's constructors get called. The relevant ones are (neither the explicit
ness and constexpr
ness of each constructor is relevant, so I am omitting them for brevity):
tuple( const Types&... args ); // (2)
template< class... UTypes >
tuple( UTypes&&... args ); // (3)
Initializing with Bar{5, "asdf"}
would invoke constructor (3)
as the better match (both (2)
and (3)
are viable, but we get a less cv-qualified reference in (3)
), which would forward from the UTypes
into the tuple
. This is why we end up with move
.
But initializing with just {5, "asdf"}
, this constructor isn't viable because braced-init-lists have no type that can be deduced. Hence our only option is (2)
, and we end up with a copy.
The only way to fix this would be to add non-template constructors that take rvalue references to each of the Types
. But you would need 2^N-1
such constructors (all but the one that takes all const lvalue references - since that one could be deduced), so instead we end up with a design that works in all cases but is suboptimal. But since you could just specify the type you want on the call site, this isn't a huge defect.
Upvotes: 2