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

标签 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]

另一方面,如果我删除运算符 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 需要一个操作符 T 来选择操作符 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 中的错误吗?

最佳答案

这是该标准的真正杰作。

何时 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) 定义内置候选集:

For the operator ,, the unary operator &, or the operator ->, the built-in candidates set is empty. For all other operators, the built-in candidates include all of the candidate operator functions defined in 16.6 that, compared to the given operator, * have the same operator name, and * accept the same number of operands, and * accept operand types to which the given operand or operands can be converted according to 16.3.3.1, and * do not have the same parameter-type-list as any non-member candidate that is not a function template specialization.

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

For every pair of promoted arithmetic types L and R, there exist candidate operator functions of the form

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

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

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

Under the conditions specified in 11.6, as part of an initialization of an object of non-class type, a conversion function can be invoked to convert an initializer expression of class type to the type of the object being initialized. Overload resolution is used to select the conversion function to be invoked. Assuming that “cv1 T” is the type of the object being initialized, and “cv S” is the type of the initializer expression, with S a class type, the candidate functions are selected as follows:

  • The conversion functions of S and its base classes are considered. Those non-explicit conversion functions that are not hidden within S and yield type T or a type that can be converted to type T via a standard conversion sequence (16.3.3.1.1) are candidate functions. For direct-initialization, those explicit conversion functions that are not hidden within S and yield type T or a type that can be converted to type T with a qualification conversion (7.5) are also candidate functions. Conversion functions that return a cv-qualified type are considered to yield the cv-unqualified version of that type for this process of selecting candidate functions. Conversion functions that return “reference to cv2 X” return lvalues or xvalues, depending on the type of reference, of type “cv2 X” and are therefore considered to yield X for this process of selecting candidate functions.

The argument list has one argument, which is the initializer expression. [ Note: This argument will be compared against the implicit object parameter of the conversion functions. —end note ]

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

Template argument deduction is done by comparing the return type of the conversion function template (call it P) with the type that is required as the result of the conversion (call it A; see 11.6, 16.3.1.5, and 16.3.1.6 for the determination of that type) as described in 17.8.2.5. ...

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

选择是否调用operator uint8_toperator L,重载解析过程与foo<uint8_t>一起使用object 作为隐含的对象参数。自 foo<uint8_t> 转换以来到隐含对象参数类型只是两种情况下的身份转换(因为两个运算符都是没有 cv 限定的直接成员),必须使用决胜规则 [over.match.best]/(1.4):

the context is an initialization by user-defined conversion (see 11.6, 16.3.1.5, and 16.3.1.6) and the standard conversion sequence from the return type of F1 to the destination type (i.e., the type of the entity being initialized) is a better conversion sequence than the standard conversion sequence from the return type of F2 to the destination type ...

因此,编译器将始终选择 operator L超过operator uint8_t为了获得从转换运算符的结果到 L 的恒等转换(除非 L 本身是 uint8_t ,但这不可能发生,因为 L 必须是升级类型)。

因此,对于每个可能的L,“调用”operator* LR(L, R) ,第一个参数所需的隐式转换序列是调用 operator 的用户定义转换L。比较时operator*具有不同的L,编译器无法决定哪一个是最好的:换句话说,它是否应该调用 operator int调用operator*(int, unsigned int) ,或者应该调用operator unsigned int调用operator*(unsigned int, unsigned int) ,或者应该调用operator double调用operator*(double, unsigned int) , 等等?所有这些都是同样好的选择,并且过载是不明确的。因此,Clang 是对的,GCC 有错误。

关于c++ - 模板化转换运算符的重载解析,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56798065/

相关文章:

c - 是否可以使用 C11 的 _Generic 实现 GNU C 的 typeof(x)?

c - "iteration ... invokes undefined behavior [-Waggressive-loop-optimizations]"警告的不明显原因

c++ - 要求程序员多久编写一次 makefile 文件?

templates - 为什么 Kendo-UI 模板没有输出?

php - Blade 模板递归包括

c++ - 如何创建仅针对特定模板特化存在的函数

c - 在没有文件的情况下链接 gcc 中的共享库?

c++ - 帧缓冲对象错误?

c++ - void() 用作模板参数时意味着什么?

c++ - 使用 RegQueryValueEx 和 HKEY_PERFORMANCE_COUNTER 获取“磁盘字节数/秒%