首页 > 解决方案 > 模板化转换运算符的重载解决方案

问题描述

这段代码:

#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;
}

编译(使用 gcc 9.1.0)并打印

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 Tfor operator S。但是,gcc 就在这里吗?Clang 8.0 抱怨

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中的错误吗?

标签: c++templatesgccoperator-overloadinglanguage-lawyer

解决方案


这是该标准的真正巡回演出。

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) 定义了一组内置候选:

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

16.6/13 中定义的内置候选项为operator*

对于每对提升的算术类型LR,存在形式为的候选运算符函数

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

Clang 正在打印出此类内置候选者的完整列表。大概 GCC 同意这个列表。现在必须应用重载决议来选择要“调用”的那个。(当然,内置operator*函数不是真正的函数,所以“调用”它只是意味着将参数转换为“参数”类型,然后执行内置的乘法运算符。)显然,最可行的候选者应该是R这样unsigned int我们就可以得到第二个参数的精确匹配,但是第一个参数呢?

对于给定的L,编译器必须使用 [over.match.conv] 中描述的候选者递归地应用重载决议,以确定如何转换foo<uint8_t>L

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

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

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

foo<uint8_t>因此,转换为L的一个候选者是调用operator uint8_t然后执行转换uint8_tL所需的任何标准转换。另一个候选者是调用operator S,但S必须按照 [temp.deduct.conv] 中的指定推导:

模板参数推导是通过将转换函数模板(调用它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 L overoperator uint8_t以便从转换运算符的结果到L的身份转换(除非L本身是uint8_t,但这不可能发生,因为L必须是提升的类型)。

因此,对于每个可能的L,到 "call" operator* LR(L, R),第一个参数所需的隐式转换序列是调用operator L的用户定义转换。当比较operator*s 和不同的L时,编译器无法决定哪个是最好的:换句话说,它应该调用operator intcall operator*(int, unsigned int),还是应该调用operator unsigned intcall operator*(unsigned int, unsigned int),或者应该调用operator doublecall operator*(double, unsigned int),等等? 所有都是同样好的选择,而且过载是模棱两可的。因此,Clang 是正确的,而 GCC 有一个错误。


推荐阅读