Clang 中的运算符重载不明确

Ambiguous operator overload in Clang

本文关键字:重载 不明确 运算符 Clang      更新时间:2023-10-16

请考虑以下事项:

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

也就是说,如果operator +是在类模板内部而不是外部定义的,则 Clang 会在代码中存在的两个operator +之间产生歧义。GCC 仍然可以正常编译。

为什么会这样?这是 GCC 或 Clang 中的错误吗?

这是 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模棱两可。

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

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

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

编辑:这个答案的原始版本说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>的左值引用",因为该函数没有引用限定符。所以这两个参数列表都是(C<D>&, E)的。由于这无法在两个重载之间进行选择,因此部分排序测试将再次启动。

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

如果只有一个函数模板M是 某些类AM被认为是在其函数参数列表中插入了一个新的第一个参数。给定cv作为M的 cv 限定符(如果有),新参数的类型为 ">对 cvA的右值引用",如果可选Mref 限定符&&,或者如果M没有ref 限定符并且另一个模板的第一个参数具有 rvalue 引用类型。否则,新参数的类型为">对 cvA的左值引用"。[注意:这允许 非静态成员相对于非成员函数进行排序,并且结果等效 订购两个等效的非成员。— 尾注]

据推测,这是GCC和Clang对标准做出不同解释的步骤。

我的看法:成员operator+已经在类C<D>中找到。未推导类C的模板参数T;之所以知道,是因为名称查找过程进入了D的具体基类C<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作为自由参数,那么 GCC 继续选择非成员重载的决定是有意义的。