I'm running into an issue where under a specific set of conditions, a local variable being returning by a function is copied instead of moved. So far, it seems like it needs to meet the following:
- The returned variable has some usage in the function. I'm assuming otherwise the whole copy/move is ellided.
- The returned type is using a perfect forwarding-style constructor. From this answer (Usage of std::forward vs std::move) I learned that this style of constructor has some different deduction rules.
- Be compiled in gcc before 8.0. Compiling under clang (and apparently gcc 8.0+, thanks PaulMcKenzie for the comment) produces the results I expect, however I'm not free to change the compiler being used in the larger project.
Here's some minimum reproduction:
#include <iostream>
#include <type_traits>
// Test class to print copies vs. moves.
class Value
{
public:
Value() : x(0) {}
Value(Value&& other) : x(other.x)
{
std::cout << "value move" << std::endl;
}
Value(const Value& other) : x(other.x)
{
std::cout << "value copy" << std::endl;
}
int x;
};
// A container class using a separate lvalue and rvalue conversion constructor.
template<typename T>
class A
{
public:
A(const T& v) : data_(v)
{
std::cout << "lvalue conversion" << std::endl;
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
A(T&& v) : data_(std::move(v))
{
std::cout << "rvalue conversion" << std::endl;
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
T data_;
};
// A container class using a single perfect forwarding constructor.
template<typename T>
class B
{
public:
template <typename U>
B(U&& v) : data_(std::forward<U>(v))
{
std::cout << "template conversion" << std::endl;
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
T data_;
};
// Get a Value rvalue.
Value get_v()
{
Value v;
v.x = 10; // Without this things get ellided.
return v;
}
// Get an A<Value> rvalue.
A<Value> get_a()
{
Value v;
v.x = 10; // Without this things get ellided.
return v;
}
// Get a B<Value> rvalue.
B<Value> get_b()
{
Value v;
v.x = 10; // Without this things get ellided.
return v;
}
int main()
{
Value v = Value();
std::cout << "--------\nA" << std::endl;
std::cout << "lvalue" << std::endl;
A<Value> a0(v);
std::cout << a0.data_.x << std::endl;
std::cout << "rvalue" << std::endl;
A<Value> a1(get_v());
std::cout << a1.data_.x << std::endl;
std::cout << "get_a()" << std::endl;
std::cout << get_a().data_.x << std::endl;
std::cout << "--------\nB" << std::endl;
std::cout << "lvalue" << std::endl;
B<Value> b0(v);
std::cout << b0.data_.x << std::endl;
std::cout << "rvalue" << std::endl;
B<Value> b1(get_v());
std::cout << b1.data_.x << std::endl;
std::cout << "get_b()" << std::endl;
std::cout << get_b().data_.x << std::endl;
return 0;
}
Under gcc this produces:
--------
A
lvalue
value copy
lvalue conversion
A<T>::A(const T&) [with T = Value]
0
rvalue
value move
rvalue conversion
A<T>::A(T&&) [with T = Value]
10
get_a()
value move <---- Works with separate constructors.
rvalue conversion
A<T>::A(T&&) [with T = Value]
10
--------
B
lvalue
value copy
template conversion
B<T>::B(U&&) [with U = Value&; T = Value]
0
rvalue
value move
template conversion
B<T>::B(U&&) [with U = Value; T = Value]
10
get_b()
value copy <---- Not what I expect!
template conversion
B<T>::B(U&&) [with U = Value&; T = Value]
10
For completeness, clang gives:
--------
A
lvalue
value copy
lvalue conversion
A<Value>::A(const T &) [T = Value]
0
rvalue
value move
rvalue conversion
A<Value>::A(T &&) [T = Value]
10
get_a()
value move
rvalue conversion
A<Value>::A(T &&) [T = Value]
10
--------
B
lvalue
value copy
template conversion
B<Value>::B(U &&) [T = Value, U = Value &]
0
rvalue
value move
template conversion
B<Value>::B(U &&) [T = Value, U = Value]
10
get_b()
value move <---- Like this!
template conversion
B<Value>::B(U &&) [T = Value, U = Value]
10
I have two questions:
- Is this allowed behavior by gcc?
- Is there any way to force move behavior here through changing the implementation of A/B? It seems any time you have a templated function parameter being taken as && it will trigger the special rules for perfect forwarding, so if I try to provide two constructors as in the A example, one taking const U& and one taking U&&, it won't avoid the problem as long as they have other templating in place.