8
#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&&’
4

1 回答 1

10

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

Myclass b(a);

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

首先要注意的是这两个构造函数都是可行的:像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&&仅当传递右值时,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++;}

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

实际上,当传递左值时,T将推断X&为某些XMyclass在您的情况下传递的表达式的类型),T&&并将解析为X&; 另一方面,当传递 rvalue 时,T将推断为X来自 some XMyclass在您的情况下是您传递的表达式的类型),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);
}

这是一个活生生的例子

于 2013-06-08T11:28:48.703 回答