I am building a template class that must be able to hold a value or a reference. I am building the library for C++11 compatibility.
Here is the relevant code for the class
template<typename T>
class Foo
{
private:
T _Data;
public:
Foo(const T& i_Data) :
_Data(i_Data)
{}
virtual ~Foo()
{}
Foo(const Foo &i_Other):
_Data(i_Other._Data)
{};
Foo(Foo &&io_Other):
_Data(std::move(io_Other._Data))
{}
// etc..
};
The code works excepted when creating a Foo object in this way:
double Bar = 2.5;
Foo<double&> f = Bar; // This does not compile (in c++11, C++20 works though)
// error: cannot bind non-const lvalue reference of type ‘double&’ to an rvalue of type ‘std::remove_reference<double&>::type’ {aka ‘double’}
Foo<double&> f(Bar); // This does compile
I figured that when using the first line, the compiler tries to build a temporary Foo, and then move this into the Foo, although i don't understand why and how to cleanly avoid this (Of course, i could remove the move constructor but that's a cheesy fix)
Also, why does it work with C++17 and not C++11. From what i can see this is not a problem related to template deduction so this should work the same in both standards right?
In Foo<double&>
, the constructor taking const T&
would have a type you probably didn't expect as the result of reference collapsing. See also What are the reference collapsing rules, and how are they utilized by the C++ standard library?
You are forming a "const lvalue reference to lvalue reference to double
" which collapses into "reference to double
", so your constructor is effectively:
Foo(double& i_Data)
Thus,
Foo<double&> f = Bar;
... is perfectly fine, since Bar
is an lvalue.
Prior to C++17, there was no mandatory copy elision in this case, and the above line would implicitly convert Bar
to Foo<double&>
and then use the move constructor, but your move constructor doesn't handle T = double&
properly.
You attempt to bind the _Data
member (of type double&
) to a double&&
returned by std::move
, and that's not allowed.
The compiler was allowed to elide this copy and the call to the move constructor, but at least on paper, the call has to be valid.
Since C++17, copy-initialization simply calls Foo(const T&)
directly and does not create any temporary objects, so the move constructor is irrelevant.
However, you would run into trouble if you actually tried to use the move constructor just as in C++11.
Disallow the use of references for Foo
. Things get really weird when reference collapsing gets involved, and it's most likely more trouble than it's worth to support the case where T
is a reference.
Also note that _Data
is a reserved name. See also What are the rules about using an underscore in a C++ identifier?