Search code examples
c++visual-c++variable-assignment

C2440: 'initializing': cannot convert from 'A<double>' to 'A<double>'


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 0s are changed to #if 1s (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?


Solution

  • 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 rvalues that are relevant here:

    • Address of an rvalue cannot be taken by built-in address-of operator: &int(), &i++, &42, and &std::move(x) are invalid
    • An rvalue may be used to initialize a const lvalue reference, in which case the lifetime of the object identified by the rvalue is extended until the scope of the reference ends

    This 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.

    Copy elision wrinkle

    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.