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

Correct behavior of built-in operator candidates of overload resolution in the operator expression context

本文关键字:运算符 选项 内置 上下文 表达式 重载 候选项      更新时间:2023-10-16

目前我试图理解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 进行解释。

CWG 1687引入了C++14的变化。

我的幼稚解释

我最初认为顶级代码在 C++14 中应该格式不正确。根据标准,我对顶级代码重载分辨率过程的天真理解是这样的(节号来自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

选择内置运算符时,应用上面引用的规则 (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 应用于参数,这是一个错误。

海湾合作委员会的解释

GCC 选择不带诊断#1。Visual Studio 2017 中的Microsoft C/C++编译器似乎行为相同。

此外,这种行为对我来说似乎是合理的(编辑:见[1])。

我的猜测是GCC认为#2是不可行的,然后只有可行的功能是#1的。但是我找不到任何规则,例如内置运算符在用户定义的转换序列中没有第二个标准转换序列的情况下变得格式不正确时是不可行的。事实上,当CWG 1687引入短语">除了用户定义的转换序列的第二个标准转换序列"时,似乎在可行性的定义中没有其他修改。

问题

问题1:根据现行标准,哪种解释是正确的?

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


脚注

  • [1]:为了不以静默方式破坏 C++03 中编写的现有代码,这种行为是不希望的。这可能就是CWG 1687决定禁用第二个标准转换序列而保留可行性定义的原因。请参阅下面的评论。

更新

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

  • 海湾合作委员会 GCC 81789
  • 叮当 34138
  • MSC VisualStudio 92207

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

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

#1需要两个用户定义的转化序列,#2需要一个标准转换序列(完全匹配,但无关紧要)和一个用户定义的转换序列。对于第一个参数,标准转换序列优于用户定义的转换序列,而对于第二个参数,两个序列是无法区分的(这些条件都不适用)。由于#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、该问题中的示例以及标准编译中的示例。