Clang中的操作符过载不明确

ale*_*cov 14 c++ gcc templates 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; }
Run Code Online (Sandbox Code Playgroud)

此代码在GCC-7和Clang-5上编译良好.选定的重载operator +struct E.

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

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

即,如果operator +被定义内部类模板,代替,再产生锵歧义之间的两个operator +S出现在代码中.海湾合作委员会仍然编制好.

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

Bri*_*ian 7

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

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

template<typename T, typename U>
void operator +(C<T>&, U);
template<typename T>
void operator +(C<T>&, E);
Run Code Online (Sandbox Code Playgroud)

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

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

void(C<X1>&, X2)
Run Code Online (Sandbox Code Playgroud)

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

void(C<X3>&, E)
Run Code Online (Sandbox Code Playgroud)

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

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

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

如果只有一个函数模板M是某个类的非静态成员A,M则认为在其函数参数列表中插入了新的第一个参数.鉴于CV 为一体的cv修饰符M(如果有的话),新的参数的类型是"右值参考CV A "如果可选的 REF-限定符M&&,或者如果M没有REF-限定符和其它模板的第一参数具有右值参考类型.否则,新参数的类型为"对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作为自由参数,然后才开始偏序测试.


eca*_*mur 6

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

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

C<D>::template<class U> void operator+(U);  // #1
Run Code Online (Sandbox Code Playgroud)

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

什么时候应该转变成

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

我们可以看到,与你的相比

template<class T> void operator+(C<T>&, E);  // #2
Run Code Online (Sandbox Code Playgroud)

#2比错误好#1a,但是含糊不清#1b.

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

template<class> struct C;
struct D;
template<> struct C<D> {
    // ...
Run Code Online (Sandbox Code Playgroud)

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