【发布时间】:2017-08-09 23:55:22
【问题描述】:
目前我正在尝试理解 C++ 标准中的 [over.match.oper]/7 段落,但遇到了以下情况,其中 GCC 和 Clang 产生不同的结果:
https://wandbox.org/permlink/WpoMviA4MHId7iD9
#include <iostream>
void print_type(int) { std::cout << "int" << std::endl; }
void print_type(int*) { std::cout << "int*" << std::endl; }
struct X { X(int*) {} };
struct Y { operator double() { return 0.0; } };
int operator+(X, int) { return 0; } // #1
// T* operator+(T*, std::ptrdiff_t); // #2: a built-in operator (N4659 16.6/14)
int main() {
int* p = 0;
Y y;
print_type(p + y); // This line produces different results for different compilers:
// - gcc HEAD 8.0.0 : always "int" (#1 is called)
// - clang HEAD 6.0.0 : always "int*" (#2 is called)
// - my understanding : "int*" until C++11, ill-formed since C++14
return 0;
}
标准中的描述
以下是标准版本中相应段落的引用:
C++1z (N4659) 16.3.1.2 [over.match.oper] 第 7 段
(与C++14 (N4140) 13.3.1.2 [over.match.oper] 第7段基本相同):
如果通过重载决议选择了内置候选,则将类类型的操作数转换为所选操作函数的相应参数的类型,但用户定义的转换序列的第二个标准转换序列( 16.3.3.1.2) 不适用。然后该操作符被视为对应的内置操作符并根据第 8 条进行解释。 [示例:
struct X { operator double(); }; struct Y { operator int*(); }; int *a = Y() + 100.0; // error: pointer arithmetic requires integral operand int *b = Y() + X(); // error: pointer arithmetic requires integral operand- 结束示例]
C++03 13.3.1.2 [over.match.oper] 第 7 段
(与C++11 (N3291) 13.3.1.2 [over.match.oper] 第7段基本相同):
如果通过重载决议选择了内置候选,则操作数将转换为所选操作函数的相应参数的类型。然后该操作符被视为对应的内置操作符,并根据第 5 条进行解释。
C++14 的变化是由CWG 1687 介绍的。
我的幼稚解读
我最初认为 C++14 中的顶级代码应该是格式错误的。按照标准,我对top code重载解析过程的幼稚理解是这样的(节号来自N4659):
首先生成候选函数集。它包含用户定义的运算符#1 (16.3.1.2/(3.2)) 和一个内置的运算符#2 (16.3.1.2/(3.3), 16.6/14)。接下来,为了确定一组可行的函数,通过为每个参数/参数对构造隐式转换序列 (ICS) 来测试两个运算符的可行性;所有的ICS都成功构造为ICS1(#1) = int* → X(16.3.3.1.2,用户定义的转换序列),ICS2(#2) = Y → double → int(用户定义的转换序列),ICS1(#2) = int* → int*(16.3.3.1/6,身份转换,标准转换之一序列)和ICS2(#2) = X → double → std::ptrdiff_t(用户定义的转换序列),因此这两个运算符都是可行的。然后,通过比较ICS选择最佳可行功能;由于ICS1(#2) 比ICS1(#1) (16.3.3.2/(2.1)) 好并且ICS2(#2) 不比ICS2(#1) (16.3.3.2/3) 差,所以#2 是一个比#1 (16.3.3/1) 更好的函数。最后,重载决议(16.3.3/2)选择了内置运算符#2。
选择内置操作员时,上面引用的规则(@ 987654333)适用:在将ICS应用于参数后,将操作员表达的处理转移到第8条[expr]。在这里,ICS 的应用在 C++11 和 C++14 中有所不同。在 C++11 中,完全应用了 ICS,所以考虑(int*) y + (std::ptrdiff_t) (double) n,就可以了。而在 C++14 中,没有应用用户定义的转换序列中的第二个标准转换序列,因此考虑了 (int*) y + (double) n。这会导致违反语义规则 (8.7/1),即表达式格式错误,需要实现才能发出诊断消息。
Clang 的解释
Clang 选择 #2 并在没有任何关于 8.7/1 违规的诊断消息的情况下调用它。我的猜测是 Clang 在将调用转移到内置规则 (8.7/1) 之前完全将 ICS 应用于参数,这是一个错误。
GCC的解释
GCC 选择 #1 而不进行诊断。 Visual Studio 2017 中的 Microsoft C/C++ 编译器的行为似乎相同。 此外,这种行为对我来说似乎是合理的(编辑:参见 [1])。
我的猜测是 GCC 认为 #2 不可行,然后只有可行的功能是 #1。但是我找不到像内置运算符这样的规则,当它在用户定义的转换序列中没有第二个标准转换序列的情况下变得不正确时,它是不可行的。事实上,当 CWG 1687 引入短语“除了用户定义的转换序列的第二个标准转换序列”时,似乎对生存能力的定义没有其他修改。 p>
问题
问题一:按照现行标准,哪个是正确的解释?
问题 2:如果我的天真解释是正确的,那么 CWG 1687 的行为是否有意?
脚注
- [1]:不要默默地破坏用 C++03 编写的现有代码,这种行为是不可取的。这可能是 CWG 1687 决定仅禁用第二个标准转换序列而保留生存能力定义的原因。请参阅下面的 cmets。
更新
在这个问题之后,以下编译器报告了此问题:
- gcc gcc 81789
- 叮当llvm 34138
- MSCvisualstudio 92207
【问题讨论】:
-
我想说 gcc 在这里是错误的。如果将其发布到版本中,它将破坏许多现有代码。该标准禁止为指针类型重载算术运算符。
-
@Michael Roy > 它会破坏很多现有代码 哦,我明白了。这就是 CWG 1687 没有修改生存能力定义的原因。谢谢!
标签: c++ operator-overloading c++14 c++17