用户定义的转换运算符优先,以 G++ 而不是 CLak++编译

user-defined conversion operators precedence, compiles in g++ but not clang++

本文关键字:G++ 编译 CLak++ 定义 转换 运算符 用户      更新时间:2023-10-16

我有以下代码,它是 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
}

让我们从 §13.3.1.2 [over.match.oper]/p2-3 开始:

如果任一操作数的类型是类或枚举,则 可以声明实现此目的的用户定义的运算符函数 可能需要运算符或用户定义的转换才能转换 操作数,为适用于内置运算符的类型。在此 在这种情况下,过载分辨率用于确定哪个运算符函数 或者调用内置算子来实现算子。

[...]

对于具有左操作数的二元运算符@其类型 CV-非限定版本是T1的,并且是其类型的右操作数 CV-不合格版本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 优于另一个函数 F2 的第一个要求是,对于每个参数,F1 所需的转换不比 F2 差 - §13.3.3 [over.match.best]/p2。

因此,没有唯一的最佳重载,重载解析失败,程序格式不正确。Clang拒绝这段代码是正确的,而g++

拒绝它失败了。

我手边没有手册,但C++更喜欢隐式类型化对象而不是从对象进行类型转换。换句话说,如果Foo<int>有一个可以接受double的构造函数,它会double + Foo<int>解释为Foo<int>(double) + Foo<int>。("可以采用double"允许将double隐式类型转换为其他内容,例如int除非有问题的构造函数被声明为explicit

如果Foo<int>没有合适的构造函数,只有这样它才会考虑调用Foo<int>::operator double()将对象降级为double......我什至不确定该语言是否会隐含地尝试!

如果你真的想先double + Foo<int> Foo<int>转换为double,然后添加,你需要写:

double operator +(double a, const Foo<int>& b)
{
    return a + double(b);
}

或某种模板等效项。只要存在Foo<int>::operator double()就不需要friend声明。