This code throws a compilation error in Visual Studio 2017:
#include <iostream>
#include <string>
using std::cin;
using std::cout;
template<class T>
class A
{
public:
A(T a);
~A() {}
#if 0
A(const A<T>&);
#else
A(A<T>&);
#endif
T t;
};
template<class T>
A<T>::A(T a) : t(a) {}
template <class T>
#if 0
A<T>::A(const A<T>& a)
#else
A<T>::A(A<T>& a)
#endif
{
t = a.t;
std::cout << "In A copy constructor.\n";
}
int main()
{
std::string s;
A<int> a1(11);
A<double> a2(2.71);
#if 1
A<double> a3 = A<double>(a2); //gives C2440 when copy constructor argument is not const.
//compiler message is: 'initializing': cannot convert from 'A<double>' to 'A<double>'
#else
A<double> a3{a2}; //works when copy constructor argument is not const.
#endif
std::cout << a3.t << "\n";
std::cout << "Press ENTER to exit.\n";
std::getline(std::cin, s);
}
The compilation fails with C2440: 'initializing': cannot convert from 'A<double>' to 'A<double>'.
When the first two #if 0
s are changed to #if 1
s (selecting the copy constructor with the const argument), the program compiles and runs. Also if #if 0
is selected for all the conditional compilations, the program compiles and runs.
This question has no answer to mine. According to cppreference.com, copy constructors with non-const arguments are possible:
A copy constructor of class T is a non-template constructor whose first parameter is T&, const T&, volatile T&, or const volatile T&, and either there are no other parameters, or the rest of the parameters all have default values.
and the copy constructor with the non-const argument works anyway when I write
A<double> a3{a2};
So why does initializing with
A<double> a3 = A<double>(a2);
not work when the argument of the copy constructor is not const?
In this situation, the MSVC error message is particularly lacking; if you run GCC over this you get the following error:
main.cpp: In function ‘int main()’:
main.cpp:42:20: error: cannot bind non-const lvalue reference of type ‘A<double>&’ to an rvalue of type ‘A<double>’
42 | A<double> a3 = A<double>(a2); //gives C2440 when copy constructor argument is not const.
| ^~~~~~~~~~~~~
main.cpp:28:15: note: initializing argument 1 of ‘A<T>::A(A<T>&) [with T = double]’
28 | A<T>::A(A<T>& a)
| ~~~~~~^
make: *** [<builtin>: main] Error 1
If you strip away all the unrelated parts of your example, you are left with:
int main() {
double &a = 2.71;
}
which still returns the same error:
main.cpp: In function ‘int main()’:
main.cpp:2:13: error: cannot bind non-const lvalue reference of type ‘double&’ to an rvalue of type ‘double’
2 | double &a = 2.71;
| ^~~~
make: *** [<builtin>: main] Error 1
To unpack this, we need to look at the different value categories in C++ and what restrictions there are on each:
An lvalue
is, broadly, something that has a name. In your original failing example, the lvalue
is the A<T> &a
parameter of the copy constructor, and in my stripped down example, it's double &a
.
An rvalue
is a bit trickier, but it's effectively something without a name. Often it's referred to as a temporary
(as @Eljay has done in their comment) because without a name, it has no lifetime, so it is destructed almost immediately (with a caveat as explained below). In your example, the rvalue
is A<double>(a2)
, and in mine it's 2.71
.
There are two parts of the linked cppreference page regarding rvalue
s that are relevant here:
&int()
, &i++
, &42
, and &std::move(x)
are invalidThis first point is what causes the issue with the copy constructor without const
being passed an rvalue
; based on the language rules, the object being referred to is no longer valid inside the constructor.
The second point is what allows the copy constructor with const
to operate when passed an rvalue
; the lifetime is extended until the reference inside the constructor ends, which allows the constructor to copy out member variables etc.
The two ways that you are initialising a3
differ as the first:
A<double> a3{a2};
only calls the copy constructor once, with an lvalue
(a2
) whereas the second:
A<double> a3 = A<double>(a2);
calls the copy constructor twice, the first time with an lvalue
(a2
) as above, the second with an rvalue
(the result of the first constructor invocation). However, the copy elision optimisation will remove one of the invocations, which can be confusing at first glance, as it may not be apparent which of those two copy constructor invocations is causing the issue.