关于c ++:重载模板类的运算符时的隐式转换

Implicit conversion when overloading operators for template classes

我想知道为什么隐式类型转换不适用于类模板上的外部运算符重载。以下是工作的非模板化版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class foo
{
public:

    foo() = default;

    foo(int that)
    {}

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

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

如预期的那样,以下行正确编译:

1
2
3
4
5
foo f, g;
f = f + g; // OK
f += 5; // OK
f = f + 5; // OK
f = 5 + f; // OK

另一方面,当类foo声明为这样的简单模板时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
}

以下行编译时出错:

1
2
3
4
5
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+)

我想了解为什么编译器(GCC4.6.2)无法使用类模板版本的转换构造函数执行隐式类型转换。这是预期的行为吗?除了手动创建所有必需的重载之外,还有其他解决方法吗?


它不仅起作用的原因是隐式类型转换(即通过构造函数)在模板参数推导期间不适用。但是,如果您使外部操作符成为朋友,那么它就会工作,因为类型t是已知的,这样编译器就可以研究什么可以强制转换,以使参数匹配。

我做了一个基于你的例子(但是删除了C++ 11的东西),灵感来自于Scott Meyers有效C++(ED 3)中的条目46(一个有理数类)。你的问题几乎与那件事完全吻合。斯科特还指出……"使用friend与访问类的非公共部分无关。"

这也将允许与foo、foo等混合使用,只要可以添加t和u等。

还看这个帖子:C++加法超载歧义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#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;            
}


我把这个问题交给了微软的图书馆作者,得到了斯蒂芬·拉瓦维的一个非常有用的回答,所以我完全相信他提供的信息。

模板案例中出现的编译错误是由于模板参数推导在重载解决方案之前运行,模板参数推导需要完全匹配才能向重载集添加任何内容。

具体来说,模板参数推导会查看每对参数类型p和参数类型a,并试图找到模板替换,使其与p完全匹配。在找到每个参数的匹配项后,它会检查一致性(因此,如果您使用t=int作为第一个参数调用bar(foo, foo),t=double作为第二个参数,它也会ILS)。只有在函数签名中成功地替换了完全一致的匹配项之后,才能将签名添加到候选函数集以进行重载解析。

只有在所有普通函数(通过名称查找找到)和匹配的函数模板签名添加到重载集之后,才运行重载解决方案,此时所有这些函数签名都将被评估为"最佳匹配",在此期间将考虑隐式转换。

对于foo + 5operator+(foo, foo)情况,模板参数推导找不到t的替代,使得表达式fooint完全匹配,从而使操作符+的重载作为候选项被抛出,甚至看不到隐式转换。

这里的观点似乎是,这通常是一件好事,因为它使模板更具可预测性,从而使奇怪的隐式行为领域成为过载解决方案。

标准对此有很多话要说:

14.8.2.1从函数调用中推导模板参数

"模板参数推导是通过将每个函数模板参数类型(称为p)与调用的相应参数的类型(称为a),如下所述。…

…一般来说,推导过程试图找到模板参数值,使推导出的与a相同(在按照上述方式转换类型a之后)

接下来将列出一些特殊情况,其中该规则有涉及cv限定符的异常(因此t&;将与const&;兼容),以及派生类的匹配(在某些情况下,它可以将派生类与基类匹配),但基本上,精确匹配是规则。


所有可能的foo都是来自int的同样有效的转换,因为构造函数采用int而不是模板类型。编译器无法使用运算符中的其他参数来猜测您可能指的是哪个参数,因此您会得到错误。如果你明确地告诉它你想要哪个实例,我相信它会起作用。