Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why are my T& and T&& copy constructors ambiguous?

#include <iostream>
using namespace std;
class Myclass{
        private:
                int i;
        public:
                template<typename U>Myclass(U& lvalue):i(lvalue){cout<<i <<" template light reference" <<endl;i++;}
                //Myclass(Myclass &lvalue):i(lvalue){cout<<i <<" light reference" <<endl;i++;}
                template<typename U>Myclass(U&& rvalue):i(rvalue){cout<<i <<" template right reference" <<endl;i++;}
};

int main(int argc,char*argv[])
{
Myclass a(0);
Myclass b(a);
Myclass c(2);
return 0;
}

error message:

rightvalue.cpp: In function ‘int main(int, char**)’:
rightvalue.cpp:15:12: error: call of overloaded ‘Myclass(Myclass&)’ is ambiguous
rightvalue.cpp:15:12: note: candidates are:
rightvalue.cpp:10:23: note: Myclass::Myclass(U&&) [with U = Myclass&]
rightvalue.cpp:8:23: note: Myclass::Myclass(U&) [with U = Myclass]
rightvalue.cpp:4:7: note: constexpr Myclass::Myclass(const Myclass&)
rightvalue.cpp:4:7: note: constexpr Myclass::Myclass(Myclass&&) <near match>
rightvalue.cpp:4:7: note:   no known conversion for argument 1 from ‘Myclass’ to ‘Myclass&&’
like image 370
yuan Avatar asked Jun 08 '13 11:06

yuan


1 Answers

So here is what happens (or rather, should happen): in order to resolve this constructor call

Myclass b(a);

The compiler has to perform overload resolution and decide, at first, which constructors are viable candidates.

The first thing to notice is that both constructors are viable: forms like T&& do not always resolve into rvalue references (that's only the case if what you are passing is an rvalue). That's what Scott Meyers calls "universal references" (notice, that this term is not standard).

When the compiler tries to perform type deduction to see if the second constructor is viable, the type T in this case will be deduced to be Myclass& - since what you are passing (a) is an lvalue; and because of the reference collapsing rules, Myclass& && gives Myclass&, so you end up with the same signature your first constructor has.

So is the call is ambiguous? As pointed out by Marc Glisse in the comments to the question, and by Jonathan Wakely in the comments to this answer, no, it should not be (as the original version of this answer claimed - mea culpa).

The reason is that a special rule in the Standard specifies that the overload accepting an lvalue reference is more specialized than the overload accepting an rvalue reference. Per paragraph 14.8.2.4/9 of the C++11 Standard:

If, for a given type, deduction succeeds in both directions (i.e., the types are identical after the transformations above) and both P and A were reference types (before being replaced with the type referred to above):

if the type from the argument template was an lvalue reference and the type from the parameter template was not, the argument type is considered to be more specialized than the other; otherwise, [...]

This means the compiler has a bug (the link to the bug report has been provided by Marc Glisse in the comments to the question).

To workaround this bug and make sure your constructor template accepting a T&& will be picked by GCC only when rvalues are being passed, you can rewrite it this way:

    #include <type_traits>

    template<typename U,
        typename std::enable_if<
            !std::is_reference<U>::value
            >::type* = nullptr>
    Myclass(U&& rvalue):i(rvalue)
    {cout<<i <<" template right reference" <<endl;i++;}

Where I added a SFINAE-constraint which makes the compiler discard this constructor from the overload set when an lvalue is being passed.

When an lvalue is passed, in fact, T will be deduced to be X& for some X (the type of the expression you pass, Myclass in your case), and T&& will resolve into X&; when an rvalue is being passed, on the other hand, T will be deduced to be X from some X (the type of the expression you pass, Myclass in your case), and T&& will resolve into X&&.

Since the SFINAE constraint checks whether T is not deduced to be a reference type and creates a substitution failure otherwise, your constructor is guaranteed to be considered only when the argument is an rvalue expression.

So to sum it all up:

#include <iostream>
#include <type_traits>

class Myclass
{
    int i;
public:
    template<typename U>
    Myclass(U& lvalue):i(lvalue)
    {
        std::cout << i <<" template light reference" << std::endl;
        i++;
    }

    template<typename U,
        typename std::enable_if<
            !std::is_reference<U>::value
            >::type* = nullptr>
    Myclass(U&& rvalue):i(rvalue)
    {
        std::cout << i <<" template right reference" << std::endl;
        i++;
    }
};

int main(int argc,char*argv[])
{
    Myclass a(0);
    int x = 42;
    Myclass b(x);
    Myclass c(2);
}

Here is a live example.

like image 175
Andy Prowl Avatar answered Sep 20 '22 08:09

Andy Prowl