Clang中的操作符过载不明确

时间:2018-01-12 03:21:03

标签: c++ templates gcc clang language-lawyer

请考虑以下事项:

template<typename T>
struct C {};
template<typename T, typename U>
void operator +(C<T>&, U);

struct D: C<D> {};

struct E {};
template<typename T>
void operator +(C<T>&, E);

void F() { D d; E e; d + e; }

此代码在GCC-7和Clang-5上编译良好。 operator +的所选重叠是struct E的重载。

现在,如果发生以下变化:

/* Put `operator +` inside the class. */
template<typename T>
struct C {
    template<typename U>
    void operator +(U);
};

也就是说,如果里面定义了,而不是之外,那么Clang会在operator +之间产生歧义在代码中。海湾合作委员会仍然编制好。

为什么会这样?这是GCC还是Clang的错误?

2 个答案:

答案 0 :(得分:7)

编辑:这个答案的原始版本说GCC是正确的。我现在认为Clang根据标准的措辞是正确的,但我可以看到GCC的解释也是正确的。

让我们看看你的第一个例子,其中两个声明是:

template<typename T, typename U>
void operator +(C<T>&, U);
template<typename T>
void operator +(C<T>&, E);

两者都是可行的,但很明显第二个模板比第一个模板更专业。所以GCC和Clang都解析了对第二个模板的调用。但是让我们一起浏览[temp.func.order]以查看为什么,在标准的措辞中,第二个模板更专业。

部分排序规则告诉我们用唯一的合成类型替换每个类型模板参数,然后对另一个模板执行演绎。在此方案下,第一个重载类型变为

void(C<X1>&, X2)

并且对第二个模板的扣除失败,因为后者仅接受E。第二种重载类型变为

void(C<X3>&, E)

并扣除第一个模板成功T = X3U = E)。由于推导仅在一个方向上成功,因此接受另一个转换类型(第一个)的模板被认为不太专业,因此,第二个重载被选为更专业的。

当第二个重载移入类C时,仍然会找到两个重载,并且重载解析过程应以完全相同的方式应用。首先,为两个重载构造参数列表,并且由于第一个重载是非静态类成员,因此插入了隐含的对象参数。根据[over.match.funcs],该隐含对象参数的类型应为“对C<T>的左值引用”,因为该函数没有ref-qualifier。所以这两个参数列表都是(C<D>&, E)。由于这无法在两个重载之间进行选择,因此部分排序测试再次启动。

[temp.func.order]中描述的偏序测试,插入一个隐含的对象参数:

  

如果只有一个功能模板M是非静态成员   某些类AM被认为在其函数参数列表中插入了新的第一个参数。鉴于 cv   作为M的cv限定符(如果有的话),新参数的类型为“对 cv A的rvalue引用”   M参考资格&&M没有参考资格且其他模板的第一个参数包含右值   参考类型。否则,新参数的类型为“对 cv A的左值引用”。 [注意:这允许一个   要针对非成员函数进行排序的非静态成员,并且结果是等效的   订购两个相当的非会员。 - 结束记录]

这可能是GCC和Clang对标准有不同解释的步骤。

我的看法:已在班级operator+中找到成员C<D>。不推导出类T的模板参数C;它是众所周知的,因为名称查找过程输入了C<D>的具体基类D。因此,提交给部分排序的实际operator+ 具有免费的T参数;它不是void operator+(C<T>&, U),而是void operator+(C<D>&, U)

因此,对于成员重载,转换后的函数类型不应该是void(C<X1>&, X2),而应该是void(C<D>&, X2)。对于非成员重载,转换后的函数类型仍然像以前一样void(C<X3>&, E)。但现在我们发现void(C<D>&, X2)不匹配非会员模板void(C<T>&, E) void(C<X3>&, E)匹配成员模板void(C<D>&, U) 。因此,部分排序失败,重载解析返回模糊结果。

GCC决定继续选择非成员重载是有意义的,如果你假设它正在构造词汇成员的转换函数类型,使其仍然void(C<X1>&, X2),而Clang将D替换为U模板,只留下{{1}}作为免费参数,开始部分订购测试之前。

答案 1 :(得分:6)

这是gcc中的一个错误;具体而言,https://gcc.gnu.org/bugzilla/show_bug.cgi?id=53499

问题是gcc将类模板成员函数的隐式对象参数视为具有依赖类型;也就是说,在功能模板部分排序gcc变换期间

C<D>::template<class U> void operator+(U);  // #1

template<class T, class U> void operator+(C<T>&, U);  // #1a (gcc, wrong)

什么时候应该转换成

template<class U> void operator+(C<D>&, U);  // #1b (clang, correct)

与您的

相比,我们可以看到
template<class T> void operator+(C<T>&, E);  // #2

#2优于错误的#1a,但与#1b不一致。

观察即使C<D>根本不是模板,gcc也会错误地接受 - 即C<D>是一个完全专业化的类模板:

template<class> struct C;
struct D;
template<> struct C<D> {
    // ...

这由[temp.func.order]/3涵盖,并在示例中进行了说明。请注意,gcc再次错误编译该示例,错误地拒绝它,但出于同样的原因。