operator []查找模板基类

时间:2016-02-12 22:34:44

标签: c++ visual-c++ gcc clang

以下代码让我们感到头疼: clang MSVC 接受以下代码,而 GCC 拒绝它。我们相信 GCC 这次是正确的,但我想在提交bug报告之前确定它。那么,operator[]查找是否有任何我不知道的特殊规则?

struct X{};
struct Y{};

template<typename T>
struct B
{
    void f(X) { }
    void operator[](X){}
};

template<typename T>
struct C
{
    void f(Y) { }
    void operator[](Y){}
};

template<typename T> struct D : B<T>, C<T> {};

int main()
{
    D<float> d;
    //d.f(X()); //This is erroneous in all compilers
    d[Y()];//this is accepted by clang and MSVC
}

上述代码在解析operator[]函数中的main调用时是否正确?

2 个答案:

答案 0 :(得分:6)

它不是100%明确问题所在的编译器。该标准涉及名称查找的许多规则(这是一个问题),但更具体地说,第13.5.5节涵盖了operator[]重载:

  

13.5.5订阅[over.sub]

     

1 - operator[]应该是一个非静态成员函数,只有一个参数。它实现了下标语法

     

postfix-expression [ expr-or-braced-init-list ]

     

因此,如果存在x[y]并且选择了运算符,则对于x.operator[](y)类型的类对象x,下标表达式T将被解释为T::operator[](T1)作为重载解析机制(13.3.3)的最佳匹配函数。

查看重载标准(第13章):

  

13超载[over]

     

1 - 当为同一范围内的单个名称指定了两个或更多不同的声明时,该名称被称为重载。通过扩展,在同一范围内声明相同名称但具有不同类型的两个声明称为重载声明。只有函数和函数模板声明可以重载;变量和类型声明不能重载。

     

2 - 当在调用中使用重载函数名时,通过比较使用点处的参数类型和可见的重载声明中的参数类型来确定正在引用哪个重载函数声明在使用点。此函数选择过程称为重载决策,在13.3中定义。

     

...

     

13.2声明匹配[over.dcl]

     

1 - 如果两个函数声明属于同一范围且具有等效参数声明(13.1),则它们引用相同的函数。派生类的函数成员与基类中具有相同名称的函数成员不在同一范围内。

因此,根据这个以及关于派生类的第10.2节,由于您已声明struct D : B, C,因此BC都具有operator[]的成员函数,但不同类型,因此operator[]函数在D的范围内被重载(因为using没有operator[]也没有被D覆盖或直接隐藏在d[Y()]中})。

基于此, MSVC和Clang在其实现中不正确 ,因为d.operator[](Y()) 应该被评估为{{1}这会产生模糊的名称解析;那么问题是为什么他们会接受d[Y()]的语法

我可以看到关于下标([])语法的唯一其他区域引用了 5.2.1 部分(其中说明了下标表达式是什么)和 13.5.5 (如上所述),这意味着那些编译器正在使用其他规则来进一步编译d[Y()]表达式。

如果我们查看名称查找,我们会看到 3.4.1非限定名称查找第3段指出

  

3.4.2中描述了用作函数调用的后缀表达式的非限定名称的查找。

3.4.2声明:

  

3.4.2依赖于参数的名称查找[basic.lookup.argdep]

     

1 - 当函数调用(5.2.2)中的postfix-expression是非限定id时,可以搜索在通常的非限定查找(3.4.1)期间未考虑的其他命名空间,在那些名称空间中,可以找到命名空间范围的朋友函数或函数模板声明(11.3),否则可以找到。

     

2 - 对于函数调用中的每个参数类型T,有一组零个或多个关联的命名空间以及一组零个或多个要考虑的关联类。命名空间和类的集合完全由函数参数的类型(以及任何模板模板参数的命名空间)决定。用于指定类型的Typedef名称和using-declarations对此集合没有贡献。命名空间和类的集合按以下方式确定:

     

...

     

(2.2) - 如果T是类类型(包括联合),则其关联的类是:类本身;它所属的成员,如果有的话;及其直接和间接基类。其关联的命名空间是其关联类的最内部封闭命名空间。此外,如果T是类模板特化,则其关联的名称空间和类还包括:与模板类型参数(模板模板参数除外)提供的模板参数类型相关联的名称空间和类;任何模板模板参数都是成员的名称空间;以及用作模板模板参数的任何成员模板的类都是成员。 [注意:非类型模板参数不会对相关命名空间的集合做出贡献。-end note]

注意强调可能

通过以上几点以及3.4(名称查找)中的其他几点,人们可以相信Clang和MSVC首先使用这些规则来查找d[](从而将其发现为C::operator[])与使用13.5.5将d[]转换为d.operator[]并继续编译。

应该注意的是,将基类的运算符放在D类的范围内或使用显式范围确实可以修复&#39;所有三个编译器中的这个问题(正如基于引用中的using声明子句所预期的那样),例如:

struct X{};
struct Y{};

template<typename T>
struct B
{
    void f(X) { }
    void operator[](X) {}
};

template<typename T>
struct C
{
    void f(Y) { }
    void operator[](Y) {}
};

template<typename T>
struct D : B<T>, C<T>
{
    using B<T>::operator[];
    using C<T>::operator[];
};

int main()
{
    D<float> d;

    d.B<float>::operator[](X()); // OK
    //d.B<float>::operator[](Y()); // Error

    //d.C<float>::operator[](X()); // Error
    d.C<float>::operator[](Y()); // OK

    d[Y()]; // calls C<T>::operator[](Y)
    return 0;
}

由于标准最终留给了实现者的解释,我不确定在这个实例中哪个编译器在技术上是正确的,因为MSVC和Clang 可能使用其他规则来编译尽管如此,考虑到标准中的下标段落,我倾向于说它们并不像GCC那样严格遵守标准。

我希望这可以增加对问题的洞察力。

答案 1 :(得分:3)

我认为Clang和MSVC不正确,GCC拒绝此代码是正确的。这是一个原理示例,即不同范围内的名称不会相互重载。我将此作为llvm bug 26850提交给Clang,我们会看看他们是否同意。

operator[] vs f()没有什么特别之处。来自[over.sub]:

  

operator[]应该是一个非静态成员函数,只有一个参数。 [...]因此,对于x[y]类型的类对象x.operator[](y),下标表达式x被解释为T   如果T::operator[](T1)存在,并且运算符被重载选择为最佳匹配函数   解决机制

因此,管理d[Y()]查询的规则与管理d.f(X())的规则相同。所有编制者都拒绝后者是正确的,并且也应该拒绝前者。此外,两个 Clang和MSVC拒绝

d.operator[](Y());

他们都接受:

d[Y()];
尽管两者具有相同的含义。没有非成员operator[],这不是函数调用,因此也没有依赖于参数的查找。

以下是对为什么呼叫应该被视为含糊不清的解释,尽管两个继承的成员函数中的一个看起来像是更好的匹配。

<小时/> 成员名称查找的规则在[class.member.lookup]中定义。这已经有点难以解析,而且它引用C作为我们正在查找的对象(在OP中的名称为D,而C是子对象)。我们有查找集的概念:

  

fC查找集,名为S(f,C),由两个组件集组成:声明集,一套   名为f的成员;和子对象集,一组子对象,其中声明了这些成员(可能   发现包括使用声明在内。在声明集中, using-declarations 被集合替换   未被派生类(7.3.3)的成员隐藏或覆盖的指定成员,以及类型   声明(包括注入类名称)将由它们指定的类型替换。

operator[]D<float>声明集为空:既没有显式声明也没有 using-declaration

  

否则(即C不包含f的声明或结果声明集为空),S(f,C)为   最初是空的。如果C具有基类,则在每个直接基类子对象B i 中计算f的查找集,   并将每个这样的查找集S(f,B i )依次合并到S(f,C)

因此,我们会调查B<float>C<float>

  

以下步骤定义合并查找集S的结果(f,B i )   进入中间体S(f,C):    - 如果S(f,B i )的每个子对象成员是至少一个子对象的基类子对象   S(f,C)的成员,或者如果S(f,B i )为空,则S(f,C)不变并且合并完成。反过来,   如果S(f,C)的每个子对象成员是至少一个的基类子对象   S(f,B i )的子对象成员,或者如果S(f,C)为空,则新S(f,C)是S的副本(f,B i) )。
   - 否则,如果S(f,B i )和S(f,C)的声明集不同,则合并不明确:新的   S(f,C)是具有无效声明集和子对象集的并集的查找集。在随后的   合并时,认为无效的声明集与其他声明集不同    - 否则,新的S(f,C)是一个具有共享声明集和联合的查找集   子对象集。   fC的名称查找结果是S(f,C)的声明集。如果它是无效集,则程序为   病态的。 [例如:

struct A { int x; }; // S(x,A) = { { A::x }, { A } }
struct B { float x; }; // S(x,B) = { { B::x }, { B } }
struct C: public A, public B { }; // S(x,C) = { invalid, { A in C, B in C } }
struct D: public virtual C { }; // S(x,D) = S(x,C)
struct E: public virtual C { char x; }; // S(x,E) = { { E::x }, { E } }
struct F: public D, public E { }; // S(x,F) = S(x,E)
int main() {
    F f;
    f.x = 0; // OK, lookup finds E::x
}
     

S(x, F)是明确的,因为A的{​​{1}}和B基础子对象也是D的基础子对象,因此E   在第一个合并步骤中被丢弃。 -end example]

所以这里发生了什么。首先,我们尝试将S(x,D)operator[]的空声明集与D<float>的空声明集合并。这给了我们B<float>集。

接下来,我们将其与{operator[](X)}operator[]的声明集合并。后一个声明集是C<float>。这些合并集不同,因此合并为不明确。请注意,此处不考虑重载分辨率 。我们只是查找名称。

顺便说一句,修复是将 using-declarations 添加到{operator[](Y)},这样就没有完成合并步骤:

D<T>
相关问题