模板类中重载运算符时的隐式类型转换

21

我想知道为什么类模板的外部运算符重载不支持隐式类型转换。这里是可工作的非模板版本:

class foo
{
public:

    foo() = default;

    foo(int that)
    {}

    foo& operator +=(foo rhs)
    {
        return *this;
    }
};

foo operator +(foo lhs, foo rhs)
{
    lhs += rhs;
    return lhs;
}

如预期所料,以下行编译正确:
foo f, g;
f = f + g; // OK
f += 5; // OK
f = f + 5; // OK
f = 5 + f; // OK

另一方面,当类foo被声明为一个简单的模板时,如下所示:
template< typename T >
class foo
{
public:

    foo() = default;

    foo(int that)
    {}

    foo& operator +=(foo rhs)
    {
        return *this;
    }
};

template< typename T >
foo< T > operator +(foo< T > lhs, foo< T > rhs)
{
    lhs += rhs;
    return lhs;
}

以下代码行存在编译错误:
foo< int > f, g;
f = f + g; // OK
f += 5; // OK
f = f + 5; // Error (no match for operator+)
f = 5 + f; // Error (no match for operator+)

我希望了解为什么编译器(GCC 4.6.2)无法使用模板类的转换构造函数执行隐式类型转换。这是预期行为吗?除了手动创建所有必要的重载外,是否有任何解决方法?

1
你可以为 T 添加一个重载: foo< T > operator +(foo< T > lhs, T rhs) - David Feurle
3个回答

14
它不能“只是工作”的原因是,在模板参数推导期间,隐式类型转换(即通过构造函数)不适用。但是,如果您使外部运算符成为友元,则可以使类型T为已知,从而允许编译器调查可以进行转换以使参数匹配的内容。我根据您的示例(但删除了C++11内容),受Scott Meyers Effective C ++(第3版)中第46项(有理数类)启发制作了一个示例。您的问题几乎与该项目完全匹配。Scott还指出,“这种使用友元与访问类的非公共部分无关。”这也将允许混合foo ,foo 等的工作,只要可以添加等T和U。另请参阅此帖子:C++ addition overload ambiguity
#include <iostream>

using namespace std;

template< class T >
class foo
{
private:
   T _value;
public:
   foo() : _value() {}

   template <class U>
   foo(const foo<U>& that) : _value(that.getval()) {}

   // I'm sure this it can be done without this being public also;
   T getval() const { return _value ; }; 

   foo(const T& that) : _value(that) {}

   friend const foo operator +(foo &lhs,const foo &rhs) 
      {
     foo result(lhs._value+rhs._value); 
     return result;
      };
   friend const foo operator +(foo &lhs,const T &rhsval) 
      {
     foo result(lhs._value+rhsval); 
     return result;
      };
   friend const foo operator +(const T &lhsval,foo &rhs) 
      {
     foo result(lhsval+rhs._value); 
     return result;
      };

   friend foo& operator +=(foo &lhs,const foo &rhs)
      {
     lhs._value+=rhs._value;
     return lhs;
      };   
   friend std::ostream& operator<<(std::ostream& out, const foo& me){
      return out <<me._value;
   }
};

int main(){
   foo< int > f, g;
   foo< double > dd;
   cout <<f<<endl;
   f = f + g;
   cout <<f<<endl;
   f += 3 ;
   cout <<f<<endl;
   f = f + 5;
   cout <<f<<endl;
   f = 7 + f; 
   cout <<f<<endl;      
   dd=dd+f;
   cout <<dd<<endl;      
   dd=f+dd;
   cout <<dd<<endl;      
   dd=dd+7.3;
   cout <<dd<<endl;             
}

这正是我所需要的!非常感谢!我一定要拿到一份《Effective C++》…… - pmjobin
很好,我尽可能地试图写出一个与问题相关的具体答案。 - Johan Lundberg
写友元函数的离线语法是什么?如果我删除定义,就会出现“声明了一个非模板函数”的错误。 - Bill Door
1
在这里找到答案:https://dev59.com/Y0fSa4cB1Zd3GeqPA-fT?rq=1 - Bill Door

8
我将此问题提交给MS图书馆的作者,Stephan Lavavej向我提供了非常有价值的响应,因此我会全面归功于他提供的这些信息。
模板参数推导在重载解析之前运行,而在模板情况下出现的编译错误是由于模板参数推导需要精确匹配才能将任何内容添加到重载集合中。
详细来说,模板参数推导查看每个参数类型P和参数类型A,试图找到可以使A 完全匹配P的模板替换。在为每个参数找到匹配项后,它检查一致性(因此,如果您使用T = int作为第一个参数,并将T = double作为第二个参数调用bar(foo<T>, foo<T>),也会失败)。只有在成功地将精确匹配项替换到函数签名中之后,该签名才被添加到重载解析的候选函数集中。
只有在所有普通函数(通过名称查找找到)和匹配的函数模板签名都已添加到重载集合中之后,才会运行重载解析,在此期间,将考虑隐式转换来评估所有这些函数签名的“最佳匹配”。
对于带有foo<T> + 5operator+(foo<T>, foo<T>)情况,模板参数推导无法找到任何替换T的方法,使得表达式foo<T> 完全匹配int,因此该重载操作符被舍弃为候选项,并且甚至不会看到隐式转换。
这里的观点似乎是,这通常是一件好事,因为它使模板更加可预测,将奇怪的隐含行为留给重载解析。
标准文件对此有很多解释:
“从函数调用中推断模板参数的模板参数推导通过将每个函数模板参数类型(称为P)与调用的相应参数类型(称为A)进行比较来完成。……总的来说,推导过程尝试找到模板参数值,以使推导的A与A相同(在上述方式下进行的类型A变换后)”。
它继续列出了几种特殊情况,其中此规则存在关于cv限定符的异常(所以T&将与const T&兼容),以及派生类的匹配(在某些情况下,它可以将Derived&与Base&匹配),但基本上是精确匹配的规则。

2

所有可能的foo<T>都是从int进行的等效有效转换,因为构造函数接受int,而不是模板类型。编译器无法使用运算符中的其他参数来猜测您可能想要哪一个,因此会出现错误。如果您明确告诉编译器您想要哪个实例化版本,我相信它会正常工作。


但是参数是 foo<T>foo<T>,而不是 foo<T1>foo<T2>。因此,除了 int 之外的任何 T 都不会匹配第一个参数,必须被丢弃。对吗? - Ben Voigt
即使您在构造函数中将“that”的类型更改为“T”,这仍然无法正常工作(至少在MSVC10中是如此)。我试图简要查找标准中的某些银弹线来解释这一点,但我最好的猜测是涉及隐式转换的重载分辨率和模板参数推导之间发生了鸡生蛋的情况。我认为编译器在这种情况下无法完成其中一个而不另一个,因此需要更明确的重载。 - brendanw

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接