用户定义的转换运算符优先级,用g ++编译但不用clang ++编译

时间:2014-08-03 01:42:54

标签: c++ templates c++11 operator-overloading

我有以下代码,它是POD到模板类Foo<T>的包装器,其中T是包装类型(可以是intdouble等)。我定义了一个模板化转换运算符,还有朋友加法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
}

2 个答案:

答案 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,让我们构造候选集:

  • T1double,而不是类类型,因此成员候选人集合为空。
  • 非会员候选人集包括:

    Foo<int> operator+(Foo<int> lhs, const Foo<int>& rhs);
    

    (加上Foo的不同实例化的许多其他重载,这显然是比这个更糟糕的匹配。)

  • 内置候选集包含一长串函数列表,您可以在代码的clang输出中看到这些函数,在§13.6[over.built] / p12中指定:

      

    对于每对提升的算术类型LR,都存在   [...] LR operator+(L , R );形式的候选运算符函数   [...]其中LR是通常算术转换的结果   类型LR之间。

只有在这一批候选人中找到唯一的最佳匹配时,重载决议才能成功。

首先,请注意下面的众多可能的内置运算符,没有一个可能是唯一的最佳匹配,因为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()声明。