2016-02-12 66 views
26

以下代码是造成我们一点头痛:MSVC接受以下代码,而GCC拒绝它。我们认为海湾合作委员会这次是正确的,但我想在提交bugreports之前确定它。那么,有没有关于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 
} 

原来是上面的代码是在解决了main功能operator[]通话两不误?

+1

注意,通话'd.operator [](Y() );'的行为与'df(...)'完全类似,所以这些编译器有意地将不同的规则应用于用作运算符的运算符,以及通过成员访问对成员查找。 –

+0

我可能会错过一些东西,但我没有看到有任何理由允许'd [Y()]'。 – aschepler

+1

如果'B','C'和'D'不是模板,我会得到相同的结果:clang ++接受和g ++拒绝。 – aschepler

回答

3

我相信Clang和MSVC是不正确的,并且GCC拒绝这个代码是正确的。这是一个原则的例子,即不同范围内的名称不会相互重叠。我以llvm bug 26850的身份提交给Clang,我们会看看他们是否同意。

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

operator[]应该是一个只有一个参数的非静态成员函数。 [...]因此,下标表达x[y]被解释为x.operator[](y)类型T 如果T::operator[](T1)存在和的一个类对象x如果操作者由过载 解析机制选为最佳匹配函数

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

d.operator[](Y()); 

其中两个他们接受:

d[Y()]; 

尽管具有相同的含义两个。没有非成员operator[],这不是一个函数调用,所以也没有与参数相关的查找。

接下来是一个解释,为什么这个调用应该被看作是模棱两可的,尽管两个继承的成员函数中的一个看起来像是一个更好的匹配。


成员名称查找的规则在[class.member.lookup]中定义。这已经有点难以解析,加上它指的是 C作为我们正在查找的对象(其中OP中的名称为 D,而 C是子对象)。我们有这个概念 查找的设置

查找在C设置f,叫S(f,C),由两个组件集:申报设置,一组 成员命名为f;和子对象集,这是一组子对象,其中声明了这些成员(可能包括使用声明的 )。在声明集中,使用声明被替换为指定成员的集合 ,这些成员未被派生类(7.3.3)的成员隐藏或覆盖,并且类型 声明(包括注册类名称)是取而代之的是他们指定的类型。

声明中D<float>设置operator[]是空的:有没有一个明确的声明,也不是使用声明

否则(即,C不包含F的声明或声明所得集为空),S(f,C)是 最初是空的。如果C具有基类,计算所述查找在每个直接基类子对象乙, 为f设置和合并每个这样的查找集合S(F,B )依次进S(f,C)

所以我们来看看B<float>C<float>

以下步骤定义合并查找集合S的结果(F,B ) 到中间S(F,C): - 如果每个S的子对象构件(F,B (f,C)中的至少一个子对象的基类子对象,或者如果S(f,B 和)是空的,则S(f,C)不变,并且合并完成。相反,如果S(f,C)的每个子对象成员都是S(f,B 和)的子对象成员中的至少一个的基类子对象,或者S(f,C)是空,新的S(f,C)是S(f,B i)的副本。
- 否则,如果声明集S的(F,B )和S(F,C)不同,则合并是不明确的:新 S(F,C)是查找与设置无效的声明集合和子对象集合的联合。在随后的 合并中,无效声明集被认为与其他任何声明集都不相同。
- 否则,新的S(f,C)是具有共享声明集合和子对象集合的联合的查找集合。 在Cf的名称查找结果是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)是明确的,因为DAB碱子对象也都是E基地子对象,所以S(x,D) 在第一合并步骤丢弃。 -end example]

因此,这里会发生什么情况。首先,我们尝试合并D<float>中的operator[]B<float>的空声明集合。这给了我们集合{operator[](X)}

接下来,我们将其与operator[]的声明集C<float>合并。后面的这个声明集合是{operator[](Y)}。这些合并集合不同,所以合并是含糊。请注意,重载分辨率为,这里不考虑。我们只是查找名称。

的修复,顺便说一句,是添加使用申述D<T>,使得在没有合并的步骤完成:

template<typename T> struct D : B<T>, C<T> { 
    using B<T>::operator[]; 
    using C<T>::operator[]; 
}; 
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)一类对象T如果T::operator[](T1)存在类型的x并且如果操作被选择作为最佳匹配函数重载解析机制(13.3.3)。

上重载(第13章)综观标准:

13重载[过度]

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

2 - 在调用中使用重载函数名称时,引用哪个重载函数声明是通过比较使用点处的参数类型与重载声明中参数类型来确定的在使用点可见。该功能选择过程称为重载分辨率,在13.3中定义。

...

13.2宣言匹配[over.dcl]

1 - 相同名称的两个函数声明是指相同的功能,如果他们是在相同的范围,并具有相当的参数声明(13.1)。派生类的函数成员与基类中的同名函数成员不在同一个作用域中。

所以根据这一点,第10.2节上派生类,因为你已经声明struct D : B, C,二者BCoperator[]但不同的类型,因此operator[]功能的D范围内重载成员函数(因为没有using也不是operator[]覆盖或直接隐藏在D)。

在此基础上, MSVC和锵在它们的实现不正确因为d[Y()]应该进行评估,以d.operator[](Y()),这将产生模糊名称解析;所以问题是为什么他们接受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)是一个非限定标识,在通常的非限定查找(3.4.1)可能期间不考虑其他名称空间,并且在这些名称空间中,名称空间范围的友元函数或函数模板声明(11.3)可见可能被发现。

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

...

(2.2) - 如果T是一个类类型(包括工会),其相关联的类别是:类本身;它是其成员的类别(如果有的话);及其直接和间接的基类。其关联的命名空间是其关联类的最内层命名空间。此外,如果T是类模板特化,则其关联的名称空间和类还包括:与为模板类型参数提供的模板参数类型(不包括模板模板参数)关联的名称空间和类;任何模板模板参数都是成员的名称空间;以及用作模板模板参数的任何成员模板的类都是成员。 [注:非类型模板参数无助于该组对应namespaces.高端注意]

注意强调可能

随着与使用13.5.5以上几点,并从3.4(名字查找)其他几个,我们可以认为,锵和MSVC正在利用这些规则来查找d[]第一(并因此发现它作为C::operator[])将d[]转换为d.operator[]并继续编译。

应该注意的是,将基类的操作符带入D类的范围或使用显式范围会在所有三个编译器中“修复”这个问题(如预期的那样,基于引用),例如:

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和锵可能可以使用其它规则来编译这个虽然,鉴于标准的下标段落,我倾向于说他们不是严格的广告在这种情况下,像海湾合作委员会一样抨击标准。

我希望这可以增加一些洞察问题。

+2

但是'T1 :: operator @',即'D :: operator []'的合格名称查找的结果是模糊性错误,而不是重载集合。 – aschepler

+0

在操作员案例中,您引用的文字说ADL只在查找非成员时才被允许,而这些不是成员。 –

+0

我无法清楚地看到你提及的从属名称查找之间的相互作用。依赖查找应该在两种情况下都是模棱两可的(这就是为什么我认为GCC是正确的): [isocpp.org非依赖名称查找](https://isocpp.org/wiki/faq/templates#nondependent-名称查找成员) 和 [cppreference dependent names](http://en.cppreference.com/w/cpp/language/dependent_name) – u235axe