【问题标题】:Overload resolution for templated conversion operators模板化转换运算符的重载解决方案
【发布时间】:2019-06-27 20:25:17
【问题描述】:

这段代码:

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

【问题讨论】:

  • 对我来说看起来像一个 gcc 错误。在这种情况下,我不确定您的转换运算符如何不会产生模棱两可的调用。
  • @NathanOliver 是的,我后来在没有真正考虑的情况下编辑了 clang 部分。我会改变这个问题,因为同时我确信foo 不应该编译

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


【解决方案1】:

这是一个真正的标准之旅。

foo&lt;uint8_t&gt; 被实例化时,特化看起来像这样:

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

对于运算符,、一元运算符&amp; 或运算符-&gt;,内置候选集为空。对于所有其他运算符,内置候选函数包括 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&lt;uint8_t&gt; 转换为 L

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

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

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

因此,将foo&lt;uint8_t&gt; 转换为L 的一种候选方法是调用operator uint8_t,然后执行将uint8_t 转换为L 所需的任何标准转换。另一个候选是调用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&lt;uint8_t&gt; 对象一起使用作为隐含的对象参数。由于从foo&lt;uint8_t&gt; 到隐含对象参数类型的转换只是两种情况下的身份转换(因为两个运算符都是没有 cv 限定的直接成员),所以决胜局规则 [over.match.best]/(1.4 ) 必须使用:

上下文是通过用户定义的转换(参见 11.6、16.3.1.5 和 16.3.1.6)和从 F1 的返回类型到目标类型(即实体被初始化)是一个比从F2的返回类型到目标类型的标准转换序列更好的转换序列......

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-07
    • 1970-01-01
    • 1970-01-01
    • 2016-07-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多