模板化转换运算符的重载解析

时间:2019-06-27 20:25:17

标签: c++ templates gcc operator-overloading language-lawyer

此代码:

#include <iostream>
template <typename T>
void print_type(){ std::cout << __PRETTY_FUNCTION__ << '\n'; }

template <typename T>
struct foo {
    operator T(){
        std::cout << "T conversion ";         
        print_type<T>();
        return {};
    }
    template <typename S>
    operator S(){
        std::cout << "ANY conversion ";
        print_type<S>();
        return {};
    }        
};

int main(void) {
    unsigned a  = 20;
    foo<uint8_t> z;
    auto y = z*a;
}

compiles (with gcc 9.1.0) and prints

ANY conversion void print_type() [with T = int]

另一方面,如果我删除了operator T(上面没有提到):

template <typename T>
struct bar {
    template <typename S>
    operator S(){
        std::cout << "ANY conversion ";
        print_type<S>();
        return {};
    }        
};

int main(void) {
    unsigned a  = 20;
    bar<uint8_t> z;
    auto y = z*a;
}

我得到一个错误:

prog.cc: In function 'int main()':
prog.cc:19:15: error: no match for 'operator*' (operand types are 'bar<unsigned char>' and 'unsigned int')
   19 |     auto y = z*a;
      |              ~^~
      |              | |
      |              | unsigned int
      |              bar<unsigned char>

起初,我很惊讶foo要求operator T才能选择operator S。但是,gcc还在这里吗? Clang 8.0 complains with

prog.cc:24:15: error: use of overloaded operator '*' is ambiguous (with operand types 'foo<uint8_t>' (aka 'foo<unsigned char>') and 'unsigned int')
    auto y = z*a;
             ~^~
prog.cc:24:15: note: built-in candidate operator*(float, unsigned int)
prog.cc:24:15: note: built-in candidate operator*(double, unsigned int)
prog.cc:24:15: note: built-in candidate operator*(long double, unsigned int)
prog.cc:24:15: note: built-in candidate operator*(__float128, unsigned int)
[...]

...列表会继续列出各种候选人。

第一个示例为什么用gcc编译而不用clang编译?这是gcc中的错误吗?

1 个答案:

答案 0 :(得分:2)

这是该标准的真正体现。

实例化foo<uint8_t>时,专业化如下所示:

struct foo<uint8_t> {
    operator uint8_t(){
        std::cout << "T conversion ";         
        print_type<uint8_t>();
        return {};
    }
    template <typename S>
    operator S(){
        std::cout << "ANY conversion ";
        print_type<S>();
        return {};
    }
};

换句话说,该类包含一个uint8_t的非模板转换操作符和一个任意S的转换操作符模板。

当编译器看到z * a时,[over.match.oper] /(3.3)定义内置候选集:

  

对于运算符,,一元运算符&或运算符->,内置候选集为空。对于所有其他运算符,内置的候选函数包括16.6中定义的所有候选运算符功能,与给定的运算符相比,   *具有相同的运算符名称,并且   *接受相同数量的操作数,并且   *接受可以根据16.3.3.1将给定的一个或多个操作数转换为的操作数类型,以及   *与不是功能模板专业化的任何非成员候选者没有相同的参数类型列表。

在16.6 / 13中为operator*定义的内置候选是:

  

对于每对提升的算术类型LR,都存在以下形式的候选算子函数

LR operator*(L, R);
// ...

Clang正在打印出此类内置候选程序的完整列表。据推测,海湾合作委员会同意这一清单。现在必须应用重载解析来选择要“调用”的一个。 (当然,内置的operator*不是真正的函数,因此“调用”仅意味着将参数转换为“参数”类型,然后执行内置的乘法运算符。)显然,最好可行的候选人将 R 设为unsigned int,以便我们获得第二个参数的精确匹配,但是第一个参数呢?

对于给定的 L ,编译器必须对[over.match.conv]中所述的候选对象以递归方式应用重载分辨率,以确定如何将foo<uint8_t>转换为 L < / em>:

  

在11.6中指定的条件下,作为非类类型对象的初始化的一部分,可以调用转换函数以将类类型的初始化器表达式转换为要初始化的对象的类型。重载解析用于选择要调用的转换函数。假设“ cv1   T”是要初始化的对象的类型,“ cv S”是初始化表达式的类型,S是类类型,候选函数的选择如下:

     
      
  • 考虑S的转换函数及其基类。那些未隐藏在S中并且产生类型T或可以通过标准转换序列转换为类型T的类型的非显式转换函数(16.3.3.1.1)是候选函数。对于直接初始化,未隐藏在S中并产生类型T或可以通过限定转换(7.5)转换为类型T的那些显式转换函数也是如此候选函数。对于选择候选函数的过程,返回返回cv限定类型的转换函数被认为会产生该类型的cv不限定版本。返回“对 cv2 X的引用”的转换函数根据引用的类型,返回类型为“ cv2 X”的左值或x值。因此,在选择候选函数的过程中,它们被认为产生X
  •   
     

参数列表有一个参数,它是初始化器表达式。 [注意:   与转换函数的隐式对象参数进行比较。 -尾注]

因此,将foo<uint8_t>转换为 L 的一种方法是致电operator uint8_t,然后进行将uint8_t转换为 L的所有必要的标准转换。另一个候选人是打电话给operator S,但必须按照[temp.deduct.conv]中的规定推导S

  

模板参数推导是通过将转换函数模板的返回类型(称为P)与转换结果所需的类型(称为A)进行比较的;请参见11.6 ,16.3.1.5和16.3.1.6(用于确定该类型),如17.8.2.5中所述。 ...

因此,编译器将推导S = L

要选择是调用operator uint8_t还是operator L ,可以将重载解析过程与foo<uint8_t>对象一起使用作为隐含对象参数。由于从foo<uint8_t>到隐含对象参数类型的转换在两种情况下都是身份转换(因为两个运算符都是没有cv限定的直接成员),因此平局规则[over.match.best] / (1.4)必须使用:

  

上下文是通过用户定义的转换(请参见11.6、16.3.1.5和16.3.1.6)进行初始化,以及从返回类型F1到目标类型(即类型)的标准转换顺序从初始化类型F2到目标类型...

的转换顺序比标准转换顺序更好。

因此,编译器将始终选择operator上的operator uint8_t L 以从转换运算符的结果到 L (除非 L 本身是uint8_t,但这不会发生,因为 L 必须是提升型)。

因此,对于每个可能的 L ,要“调用” operator* LR(L, R),第一个参数所需的隐式转换顺序是调用operator的用户定义转换< em> L 。将operator*与不同的 L 进行比较时,编译器无法决定哪一个最好:换句话说,它应该调用operator int来调用{ {1}},还是应该调用operator*(int, unsigned int)来调用operator unsigned int,还是应该调用operator*(unsigned int, unsigned int)来调用operator double,依此类推?所有这些都是同样好的选择,并且过载是模棱两可的。因此,Clang是正确的,GCC有一个错误。