我有以下代码,它是POD到模板类Foo<T>
的包装器,其中T
是包装类型(可以是int
,double
等)。我定义了一个模板化转换运算符,还有朋友加法operator+
,请参阅下面的代码。
在main()
的最后一行,我定义Foo<int> a = 10
然后计算
cout << 2.1 + a << endl; // outputs 12
这里有什么规则?看起来表达式正在翻译为
operator+(2.1, a)
然后变为operator+(Foo<int>(2.1), a)
。为什么编译器尝试首先将a
转换为double
然后执行添加?即为什么我们没有将表达式评估为
2.1 + a.operator double()
谢谢!
PS:刚刚意识到clang++
无法编译代码,说对operator+
的重载调用是不明确的。但是g++4.9
编译它没有问题,即使打开了所有警告标志。
以下代码段:
#include <iostream>
using namespace std;
template <typename T>
class Foo // wrapper class for a POD
{
T val_; // this is the wrapped value
public:
Foo(T val = {}): val_(val) {};
template<typename S> // conversion operator
operator S ()
{
std::cout << "Calling conversion operator" << std::endl;
return val_;
}
// the += operator
Foo& operator+=(const Foo& rhs)
{
val_ += rhs.val_;
return *this;
}
// the + operator
friend Foo operator+(Foo lhs, const Foo& rhs)
{
cout << "Calling operator+" << endl;
return lhs += rhs;
}
// stream operator
friend std::ostream &operator<<(std::ostream &os, const Foo &rhs)
{
return os << rhs.val_;
}
};
int main()
{
Foo<int> a = 10;
// operator+(2.1, a), why not
// 2.1 + a. operator int() ?
cout << 2.1 + a << endl; // outputs 12
}
答案 0 :(得分:3)
让我们从§13.3.1.2开始[over.match.oper] / p2-3:
如果任一操作数的类型是类或枚举,则a 可以声明用户定义的操作符函数来实现它 可能需要运算符或用户定义的转换来转换 操作数适用于内置运算符的类型。在这 case,重载决策用于确定哪个运算符函数 或调用内置运算符来实现运算符。
[...]
表示二元运算符
@
,其左操作数的类型为 cv-unqualified version是T1
和一个类型的右操作数 cv-unqualified version是T2
,三组候选函数, 指定成员候选人,非成员候选人和内置 候选人的构建如下:
- 如果T1是完整的类类型或当前正在定义的类,那么候选成员集是合格查找的结果。
T1::operator@
(13.3.1.1.1);否则,成员候选人是 空。- 非成员候选人的集合是根据表达式在表达式上下文中
operator@
的非限定查找的结果。 非限定函数调用中通常的名称查找规则(3.4.2) 除了忽略所有成员函数。 [...]- 对于运算符
,
,一元运算符&
或运算符->
,内置候选集为空。对于所有其他运营商, 内置候选包括所有候选运算符函数 在13.6中定义,与给定的运算符相比,
- 具有相同的运营商名称,
- 接受相同数量的操作数,
- 接受可根据要转换给定操作数或操作数的操作数类型 13.3.3.1和
- 没有与非功能模板专业化的非成员候选者相同的参数类型列表。
因此,给定表达式2.1 + a
,让我们构造候选集:
T1
是double
,而不是类类型,因此成员候选人集合为空。非会员候选人集包括:
Foo<int> operator+(Foo<int> lhs, const Foo<int>& rhs);
(加上Foo
的不同实例化的许多其他重载,这显然是比这个更糟糕的匹配。)
内置候选集包含一长串函数列表,您可以在代码的clang输出中看到这些函数,在§13.6[over.built] / p12中指定:
对于每对提升的算术类型
L
和R
,都存在 [...]LR operator+(L , R );
形式的候选运算符函数 [...]其中LR
是通常算术转换的结果 类型L
和R
之间。
只有在这一批候选人中找到唯一的最佳匹配时,重载决议才能成功。
首先,请注意下面的众多可能的内置运算符,没有一个可能是唯一的最佳匹配,因为Foo<int>
可以转换为右操作数的每种可能类型:
operator+(double, unsigned long long)
operator+(double, unsigned long)
operator+(double, unsigned int)
operator+(double, __int128)
operator+(double, long long)
operator+(double, long)
operator+(double, float)
operator+(double, double)
operator+(double, long double)
operator+(double, int)
(我只列出了第一个参数类型为double
的那些,因为那是第一个参数的类型,因此其他内置函数不可能比任何一个更好。)
因此,当且仅当operator +
的重载比其中每一个都更好时,重载解析才能成功。不失一般性,我们考虑以下两个函数:
operator+(double, int); // built-in
Foo<int> operator+(Foo<int> lhs, const Foo<int>& rhs); // overload
给出(double, Foo<int>)
的参数列表。对于第一个候选者,第一个参数是完全匹配,第二个参数需要用户定义的转换。对于第二个候选者,第一个参数需要用户定义的转换,第二个参数是完全匹配。
因此,我们有一个纵横交错的情况,这意味着候选人的功能都不比另一个好。 (一个函数F1优于另一个函数的第一个要求是,对于每个参数,F1所需的转换不会比F2更差 - §13.3.3[over.match.best] / p2。)
因此,没有唯一的最佳过载,重载解析失败,并且程序格式错误。 Clang在拒绝此代码时是正确的,并且g ++在未能拒绝它时被打破。
答案 1 :(得分:1)
我没有手册,但是C ++更喜欢通过从对象进行类型转换来隐式地将转换为对象。换句话说,如果double + Foo<int>
具有可以采用Foo<int>(double) + Foo<int>
的构造函数,它会将Foo<int>
解释为double
。 (&#34;可以使用double
&#34;允许将double
隐式类型转换为其他内容,例如int
,除非声明了相关的构造函数explicit
。)击>
如果 Foo<int>
没有合适的构造函数,那么只有这样才会考虑调用Foo<int>::operator double()
将对象降级为double
...而我&# 39; m甚至不确定语言是否会隐含地尝试!
如果您真的想让double + Foo<int>
先将Foo<int>
转换为double
,然后添加,您需要写一下:
double operator +(double a, const Foo<int>& b)
{
return a + double(b);
}
或某种等效的模板。只要friend
存在,就不需要Foo<int>::operator double()
声明。