为什么我的T&和T &&复制构造函数模糊不清?

yua*_*uan 8 c++ templates c++11

#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;
}
Run Code Online (Sandbox Code Playgroud)

错误信息:

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&&’
Run Code Online (Sandbox Code Playgroud)

And*_*owl 10

所以这是发生了什么(或者更确切地说,应该发生):为了解决这个构造函数调用

Myclass b(a);
Run Code Online (Sandbox Code Playgroud)

编译器必须执行重载决策,并首先确定哪些构造函数是可行的候选者.

首先要注意的事情是,两个构造是可行的:形式像T&&总是解析为右值引用(这仅仅如此,如果你正在传递是一个右值).这就是Scott Meyers所说的"通用参考"(注意,这个术语不是标准的).

当编译器尝试执行类型推导以查看第二个构造函数是否可行时,T在这种情况下的类型将推断为Myclass&- 因为你传递的是(a)是一个左值; 而由于参考倒塌规则,Myclass& &&Myclass&,所以你最终与你的第一个构造函数具有相同的签名.

呼叫是否含糊不清?正如Marc Glisse在对该问题的评论中所指出的那样,以及Jonathan Wakely在对这个答案的评论中所指出的,,它不应该(正如这个答案的原始版本所声称的那样 - mea culpa).

原因是标准中的特殊规则指定接受左值引用的重载比接受右值引用的重载更专用.根据C++ 11标准的第14.8.2.4/9段:

如果对于给定类型,推导在两个方向上都成功(即,在上面的转换之后类型是相同的)并且P和A都是引用类型(在被替换为上面提到的类型之前):

- 如果参数模板中的类型是左值引用而参数模板中的类型不是,则参数类型被认为比其他类型更专业 ; 除此以外, [...]

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

要解决此错误并确保T&&只有在传递rvalues时,GCC才会选择接受a的构造函数模板,您可以这样重写它:

    #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++;}
Run Code Online (Sandbox Code Playgroud)

我在其中添加了SFINAE约束,这使得编译器在传递左值时从重载集中丢弃此构造函数.

实际上,当一个左值被传递时,T将被推断X&为某些X(你传递的表达式的类型,Myclass在你的情况下),T&&并将解析为X&; 另一方面,当rvalue被传递时,T将被推断为X来自某些X(你传递的表达式的类型,Myclass在你的情况下),T&&并将解析为X&&.

由于SFINAE约束检查是否T推断不是引用类型并且否则会创建替换失败,因此只有在参数是rvalue表达式时才能保证构造函数被考虑.

总而言之:

#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);
}
Run Code Online (Sandbox Code Playgroud)

这是一个实例.

  • 但正如@MarcGlisse指出的那样,[DR 1164](http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1164)说左值引用过载应该比"通用"更专业参考"一个 (2认同)