为什么我的 T& 和 T&& copy 构造函数模棱两可?

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

本文关键字:模棱两可 构造函数 我的 为什么 copy      更新时间:2023-10-16
#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;
}

错误信息:

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&&’

发生的情况是这样的(或者说,应该发生):为了解析这个构造函数调用

Myclass b(a);

编译器必须执行重载解析,并首先决定哪些构造函数是可行的候选函数。

首先要注意的是两个构造函数都是可行的:像T&&这样的形式而不是总是解析为右值引用(只有在传递的是右值时才会出现这种情况)。这就是Scott Meyers所说的"通用引用"(注意,这个术语不是标准的)。

当编译器尝试执行类型推导以查看第二个构造函数是否可行时,在这种情况下,类型T将被推导为Myclass& -因为您传递的(a)是左值;由于引用折叠规则,Myclass& &&给出了Myclass&,所以你最终得到了与第一个构造函数相同的签名。

调用是有歧义的吗?正如Marc Glisse在这个问题的评论中指出的那样,Jonathan Wakely在这个答案的评论中指出,no,它不应该是(正如这个答案的原始版本所声称的那样-我的过失)。

原因是标准中的一个特殊规则指定接受左值引用的重载比接受右值引用的重载更专门化。c++ 11标准第14.8.2.4/9段:

如果,对于给定的类型,两个方向的演绎都成功(即,转换后的类型是相同的)P和A都是引用类型(在被引用的类型替换之前)上图):

- 如果实参模板的类型是左值引用,则为形参模板的类型如果不是,则认为实参类型比另一个更专门化;否则,[…]

这意味着编译器有一个bug (bug报告的链接由Marc Glisse在问题的评论中提供)。

要解决这个错误,并确保接受T&&的构造函数模板仅在传递右值时才会被GCC选中,您可以这样重写:

    #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++;}

我添加了一个sfinae约束,使编译器在传递左值时从重载集中丢弃此构造函数。

当传递左值时,实际上,对于某些X(您传递的表达式类型为Myclass), T将被推导为X&,而T&&将解析为X&;另一方面,当传递右值时,T将从某个X(您传递的表达式的类型,在您的例子中是Myclass)推断为X, T&&将解析为X&&

由于SFINAE约束检查T是否没有被推断为引用类型,否则会创建替换失败,因此保证仅当参数是右值表达式时才考虑构造函数。

总结一下:

#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);
}

下面是一个的实例