提问者:小点点

运算符表达式上下文中重载解析的内置运算符候选的正确行为


目前我试图理解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 0313.3.1.2[over. match.oper]第7段
(与C 11(N3291)基本相同13.3.1.2[over.match.oper]第7段):

如果通过重载解析选择了内置候选,则将操作数转换为所选运算函数对应参数的类型。然后将运算符视为对应的内置运算符,并根据第5条进行解释。

CWG 1687引入了C 14的变化。

我最初认为顶部代码在C 14中应该格式不正确。根据标准,我对顶部代码过载解析过程的天真理解是这样的(节号来自N4659):

当选择了内置运算符时,上面(16.3.1.2/7)引用的规则适用:将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选择#2并在8.7/1违规时调用它而没有任何诊断消息。我猜Clang在将调用转移到内置规则(8.7/1)之前完全将ICS应用于参数,这是一个bug。

GCC选择没有诊断的#1。Visual Studio 2017中的Microsoft C/C编译器似乎行为相同。此外,这种行为在我看来是合理的(编辑:参见[1])。

我的猜测是GCC认为#2是不可行的,然后只有可行的函数是#1。但是我找不到任何这样的规则,即当内置运算符在用户定义的转换序列中没有第二个标准转换序列时变得病态时,它是不可行的。事实上,当CWG 1687引入短语“除了用户定义的转换序列的第二个标准转换序列”时,似乎在生存性的定义中没有其他修改。

问题一:按照目前的标准,哪个才是正确的解读?

问题2:如果我幼稚的解释是正确的,那么CWG 1687的行为是有意的吗?

脚注

  • [1]:不要默默地破坏用C 03编写的现有代码,这种行为是不需要的。这可能是CWG 1687决定只禁用第二个标准转换序列的原因,保留生存能力的定义。见下面的注释。

更新

在此问题之后,以下编译器报告了此问题:

  • gcc gcc 81789
  • clang llvm编译器编译器34138
  • msc可视化工作室92207

共1个答案

匿名用户

我同意你的解释。我们有int*Y类型的参数,我们有两个候选者:

operator+(X, int);                // #1
operator+(int*, std::ptrdiff_t ); // #2

#1需要两个用户定义的转换序列,#2需要一个标准的转换序列(Exact Match,尽管并不重要)和一个用户定义的转换序列。对于第一个参数,标准转换序列优于用户定义的转换序列,而对于第二个参数,这两个序列是无法区分的(这些条件都不适用)。由于#2中的第一个隐式转换序列优于#1中的第一个隐式转换序列,并且第二个转换序列是等价的,因此#2获胜。

然后在CWG 1687之后,我们不执行从doubleptrdiff_t的最后一次转换,因此结果应该是格式错误的。

要回答这个问题:

行为是CWG 1687的意图吗?

我怀疑这是肯定的,因为例子是:

int *b = Y() + X();             // error: pointer arithmetic requires integral operand

这与您的示例非常相似——唯一的区别是Y()可以转换为int*,而不是直接转换为int*。我继续提交gcc 81789和llvm编译器编译器34138。请注意,clang根本没有实现CWG 1687,该问题和标准编译中的示例。