u23*_*axe 26 c++ gcc clang visual-c++
以下代码给我们带来了一些麻烦:clang和MSVC接受以下代码,而GCC拒绝它.我们相信海湾合作委员会这次是正确的,但我想在提交错误报告之前确定.那么,有什么特殊的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
}
Run Code Online (Sandbox Code Playgroud)
那么上面的代码是否正确解析函数operator[]中的main调用?
问题所在的编译器并不是100%清楚.该标准涉及名称查找的许多规则(这是一个问题),但更具体地说,第13.5.5节涵盖了operator[]重载:
13.5.5订阅[over.sub]
1 -
operator[]应该是一个非静态成员函数,只有一个参数.它实现了下标语法
postfix-expression [ expr-or-braced-init-list ]因此,对于类型的类对象(如果存在)以及如果通过重载解析机制(13.3.3)将运算符选择为最佳匹配函数,则将下标表达式
x[y]解释为.x.operator[](y)xTT::operator[](T1)
查看重载标准(第13章):
13超载[over]
1 - 如果为同一范围内的单个名称指定了两个或更多不同的声明,则称该名称已重载.通过扩展,在同一范围内声明相同名称但具有不同类型的两个声明称为重载声明.只有函数和函数模板声明可以重载; 变量和类型声明不能重载.
2 - 当在调用中使用重载函数名时,通过比较使用点处的参数类型和在该点可见的重载声明中的参数类型来确定正在引用哪个重载函数声明使用.此功能选择过程称为过载分辨率,在13.3中定义.
...
13.2声明匹配[over.dcl]
1 - 如果两个函数声明属于同一作用域且具有等效的参数声明(13.1),则它们引用相同的函数.派生类的函数成员与基类中的同名函数成员不在同一范围内.
因此,根据这个和第10.2节关于派生类,因为你已经声明了struct D : B, C,B并且C只有operator[]不同类型的成员函数,因此operator[]函数在D(因为没有,using也没有被operator[]覆盖或直接隐藏D)的范围内被重载.
基于此,MSVC和Clang在实现中是不正确的,因为d[Y()] 应该进行评估d.operator[](Y()),这将产生模糊的名称解析; 所以,问题是为什么他们接受的语法d[Y()]可言?
我可以看到的关于subscript([])语法的唯一其他区域引用了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)的后缀表达式是一个不合格-ID,通常的不合格查找(3.4.1)中不考虑其它的命名空间可被搜索,并且在这些命名空间,名称空间范围朋友功能或函数模板声明(11.3),没有其他明显的可以被发现.
2 - 对于函数调用中的每个参数类型T,有一组零个或多个关联的命名空间以及一组零个或多个要考虑的关联类.命名空间和类的集合完全由函数参数的类型(以及任何模板模板参数的命名空间)决定.用于指定类型的Typedef名称和using-declarations对此集合没有贡献.命名空间和类的集合按以下方式确定:
...
(2.2) - 如果
T是类类型(包括联合),其关联的类是:类本身; 它所属的成员,如果有的话; 及其直接和间接基类.其关联的命名空间是其关联类的最内部封闭命名空间.此外,如果T是类模板特化,则其关联的名称空间和类还包括:与模板类型参数(模板模板参数除外)提供的模板参数类型相关联的名称空间和类; 任何模板模板参数都是成员的名称空间; 以及用作模板模板参数的任何成员模板的类都是成员.[注意:非类型模板参数不会对相关命名空间的集合做出贡献.-结束注释]
注意强调可能.
通过以上几点以及3.4(名称查找)中的其他几点,人们可以相信Clang和MSVC正在使用这些规则首先找到d[](并因此找到它C::operator[])而不是使用13.5.5来d[]转换d.operator[]并继续编译.
应该注意的是,将基类的运算符放入类的范围D或使用显式范围确实会在所有三个编译器中"修复"此问题(正如基于引用中的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;
}
Run Code Online (Sandbox Code Playgroud)
由于标准最终留给了实现者的解释,我不确定哪个编译器在这个实例中在技术上是正确的,因为MSVC和Clang 可能正在使用其他规则来编译它,但是考虑到标准的下标段落,我我倾向于说他们并没有像GCC那样严格遵守标准.
我希望这可以增加一些问题的洞察力.
我相信 Clang 和 MSVC 是不正确的,而 GCC 拒绝这段代码是正确的。这是不同范围内的名称不会互相重载这一原则的一个例子。我将此作为llvm bug 26850提交给 Clang ,我们将看看他们是否同意。
\n\noperator[]vs没有什么特别的f()。来自[over.sub]:
\n\n\n\n
operator[]应该是一个只有一个参数的非静态成员函数。[...] 因此,如果存在并且重载解析机制将运算符选为最佳匹配函数,则下标表达式x[y]将被解释为\n类型的x.operator[](y)类对象xTT::operator[](T1)
因此管理 查找的规则d[Y()]与管理 的规则相同d.f(X())。所有编译器拒绝后者都是正确的,并且也应该拒绝前者。而且, Clang 和 MSVC都拒绝
d.operator[](Y());\nRun Code Online (Sandbox Code Playgroud)\n\n他们都接受:
\n\nd[Y()];\nRun Code Online (Sandbox Code Playgroud)\n\n尽管两者具有相同的含义。没有 non-member operator[],并且这不是函数调用,因此也没有依赖于参数的查找。
下面解释了为什么该调用应该被视为不明确,尽管两个继承的成员函数之一看起来更匹配。
\n\nC我们正在查找的对象(在 OP 中被命名为D,而C是一个子对象)。我们有查找集的概念:\n\n\n\n\nin 的查找集称为,由两个组件集组成:声明集、名为 的\n 成员集;和子对象集,一组子对象,其中找到了这些成员的声明(可能\n包括使用声明)。在声明集中,using 声明被派生类成员未隐藏或覆盖的指定成员集替换 (7.3.3),并且 type 声明(包括注入类名)为替换为他们指定的类型。
\nfCS(f,C)f
in的声明集为空:既没有显式声明,也没有using-declaration。operator[]D<float>
\n\n\n否则(即
\nC不包含 f 的声明或结果声明集为空),S(f,C)则最初为空。如果有基类,则计算每个直接基类子对象 B iC中的查找集,并将每个这样的查找集 S(f,B i ) 依次合并为。fS(f,C)
所以我们研究B<float>和C<float>。
\n\n\n以下步骤定义将查找集 S(f,B i ) \n 合并到中间 S(f,C) 中的结果:\n \xe2\x80\x94 如果 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 )的副本。\n \xe2\x80\x94 否则,如果 S(f,B i ) 和 S(f,C)
\n\n
的声明集不同,则合并不明确:新的\n S(f,C) 是查找集具有无效的声明集和子对象集的并集。在随后的合并中,无效声明集被视为与任何其他声明集不同。\n \xe2\x80\x94 否则,新的 S(f,C) 是具有共享声明集和\n 子对象集并集的查找集。\n in 的名称查找结果是声明一套. 如果它是无效集,则程序格式错误。[ 例子:fCS(f,C)Run Code Online (Sandbox Code Playgroud)\n\nstruct A { int x; }; // S(x,A) = { { A::x }, { A } }\nstruct B { float x; }; // S(x,B) = { { B::x }, { B } }\nstruct C: public A, public B { }; // S(x,C) = { invalid, { A in C, B in C } }\nstruct D: public virtual C { }; // S(x,D) = S(x,C)\nstruct E: public virtual C { char x; }; // S(x,E) = { { E::x }, { E } }\nstruct F: public D, public E { }; // S(x,F) = S(x,E)\nint main() {\n F f;\n f.x = 0; // OK, lookup finds E::x\n}\n\n
S(x, F)是明确的,因为 的A和B基子对象D也是 的基子对象E,因此S(x,D)\n 在第一个合并步骤中被丢弃。\xe2\x80\x94结束示例]
所以这就是发生的事情。operator[]首先,我们尝试将in的空声明集D<float>与 的空声明集合并B<float>。这给了我们集合{operator[](X)}。
operator[]接下来,我们将其与in的声明集合并C<float>。后一个声明集是{operator[](Y)}. 这些合并集不同,因此合并是不明确的。请注意,此处不考虑重载决策。我们只是简单地查找名字。
顺便说一下,修复方法是添加using 声明,D<T>这样就不会完成合并步骤:
template<typename T> struct D : B<T>, C<T> {\n using B<T>::operator[];\n using C<T>::operator[];\n};\nRun Code Online (Sandbox Code Playgroud)\n