这应该编译吗?重载解析和隐式转换

Should this compile? Overload resolution and implicit conversions

本文关键字:转换 重载 编译      更新时间:2023-10-16

这个例子似乎用VC10和gcc编译(尽管我的gcc版本很旧(。

编辑:R. Martinho Fernandez在gcc 4.7上尝试了这一点,行为仍然相同。

struct Base
{
    operator double() const { return 0.0; }
};
struct foo
{
    foo(const char* c) {}
};
struct Something : public Base
{
    void operator[](const foo& f) {}
};
int main()
{
    Something d;
    d["32"];
    return 0;
}

但叮当抱怨道:

test4.cpp:19:6: error: use of overloaded operator '[]' is ambiguous (with operand types 'Something' and 'const char [3]')
    d["32"]
    ~^~~~~
test4.cpp:13:10: note: candidate function
    void operator[](const foo& f) {}
         ^
test4.cpp:19:6: note: built-in candidate operator[](long, const char *)
    d["32"]
     ^
test4.cpp:19:6: note: built-in candidate operator[](long, const restrict char *)
test4.cpp:19:6: note: built-in candidate operator[](long, const volatile char *)
test4.cpp:19:6: note: built-in candidate operator[](long, const volatile restrict char *)

重载分辨率通过查看此表达式考虑了两个可能的函数:

  • 调用 Something::operator[](在用户定义的转换之后(
  • 调用 const char* 的内置运算符(想想"32"[d]((在用户定义的转换和标准转换加倍到长整型之后(。

如果我d["32"]写成d.operator[]("32"),那么重载分辨率甚至不会看选项 2,并且 clang 也可以很好地编译。

编辑:(澄清问题(

这似乎是过载分辨率中的一个复杂领域,因此,我将非常感谢详细解释本例中过载分辨率的答案,并引用标准(如果有一些晦涩/高级可能是未知的规则(。

如果 clang 是正确的,我也想知道为什么两者模棱两可/一个不优于另一个。答案可能必须解释重载解析如何考虑两个候选选项上涉及的隐式转换(用户定义转换和标准转换(,以及为什么一个不比另一个更好。

注意:如果运算符 double(( 更改为运算符 bool((,则所有三个(clang、vc、gcc(都将拒绝编译,并出现类似的歧义错误。

通过逐步浏览,应该更容易理解为什么过载分辨率不明确。

§13.5.5 [over.sub]

因此,如果存在T::operator[](T1)并且重载解析机制 (13.3.3( 将运算符选为最佳匹配函数,则下标表达式x[y]被解释为类型为 x T 类型的类对象的x.operator[](y)

现在,我们首先需要一个过载集。它是根据§13.3.1构造的,包含成员和非成员函数。有关更详细的解释,请参阅我的此答案。

§13.3.1 [over.match.funcs]

p2 候选函数集可以包含要针对同一参数列表解析的成员函数和非成员函数。为了使参数和参数列表在此异构集中具有可比性,成员函数被视为具有一个额外的参数,称为隐式对象参数,它表示已为其调用成员函数的对象。[...]

p3 同样,在适当的时候,上下文可以构造一个参数列表,其中包含一个隐含的对象参数来表示要操作的对象。

// abstract overload set (return types omitted since irrelevant)
f1(Something&, foo const&); // linked to Something::operator[](foo const&)
f2(std::ptrdiff_t, char const*); // linked to operator[](std::ptrdiff_t, char const*)
f3(char const*, std::ptrdiff_t); // linked to operator[](char const*, std::ptrdiff_t)

然后,构造一个参数列表:

// abstract argument list
(Something&, char const[3]) // 'Something&' is the implied object argument

然后针对重载集的每个成员测试参数列表:

f1 -> identity match on argument 1, conversion required for argument 2
f2 -> conversion required for argument 1, conversion required for argument 2 (decay)
f3 -> argument 1 incompatible, argument 2 incompatible, discarded

然后,由于我们发现需要隐式转换,我们来看看§13.3.3 [over.match.best] p1

按如下方式定义ICSi(F)

  • 如果F是静态成员函数,则 [...];否则,
  • 让我们ICSi(F)表示将列表中的 i -th 参数转换为可行函数 Fi -th 参数类型的隐式转换序列。 13.3.3.1 定义了隐式转换序列,13.3.3.2 定义了一个隐式转换序列比另一个更好的转换序列或更差的转换序列的含义。

现在让我们为重载集 (§13.3.3.1( 中的f1f2构造这些隐式转换序列:

ICS1(f1): 'Something&' -> 'Someting&', standard conversion sequence
ICS2(f1): 'char const[3]' -> 'foo const&', user-defined conversion sequence
ICS1(f2): 'Something&' -> 'std::ptrdiff_t', user-defined conversion sequence
ICS2(f2): 'char const[3]' -> 'char const*', standard conversion sequence

§13.3.3.2 [over.ics.rank] p2

标准转换序列 (13.3.3.1.1( 是比用户定义的转换序列或省略号转换序列更好的转换序列。

所以ICS1(f1)ICS1(f2)好,ICS2(f1)ICS2(f2).
差相反,ICS1(f2)ICS1(f1)差,ICS2(f2)ICS2(f1)好。

§13.3.3 [over.match.best]

p1 (续( 给定这些定义,一个可行的函数F1被定义为比另一个可行的函数更好的函数F2如果对于所有参数iICSi(F1)不是比ICSi(F2)更差的转换序列,然后[...]

p2 如果只有一个可行的函数比所有其他可行函数更好的函数,那么它就是通过重载解析选择的函数;否则调用的格式不正确。

好吧,:)因此,Clang 拒绝该代码是正确的。

似乎毫无疑问,Something::operator[](const foo& f)和内置operator[](long, const char *)都是用于解决过载的可行候选函数 (13.3.2(。真实参数的类型是Somethingconst char*,我认为隐式转换序列(ICF(是:

  • 对于Something::operator[](const foo& f):(1-1(身份转换,以及(1-2(通过foo::foo(const char*) foo("32");
  • 对于operator[](long, const char *):(2-1( long(double(d))Something::operator double() const(继承自 Base(和 (2-2( 身份转换。

现在,如果我们根据 (13.3.3.2( 对这些 ICF 进行排名,我们可以看到 (1-1( 比 (2-1( 的转换更好,(1-2( 的转换比 (2-2( 更差。根据(13.3.3(中的定义,

可行函数 F1 被定义为比另一个可行函数更好的函数 F2 如果对于所有参数 i,ICSi(F1( 不是比 ICSi(F2( 更差的转换序列,...

因此,

所考虑的两个候选函数都不比另一个更好,因此调用格式不正确。 即 Clang 似乎是正确的,代码不应该编译。

从 C++11 规范中的 13.6 来看,clang 在这里是正确的:

13,6 内置运算符 [已建成]

表示子句 5 中定义的内置运算符的候选运算符函数在 此子条款。这些候选函数参与运算符重载解析过程,如前所述 在13.3.1.2中,不用于其他目的。[ 注意:因为内置运算符只接受操作数 非类类型和运算符重载解析仅在操作数表达式最初具有类时发生 或枚举类型,运算符重载解析只有在操作数 具有一个类类型,该类类型具有用户定义的转换为适合运算符的非类类型,或者当 操作数具有枚举类型,该枚举类型可以转换为适合运算符的类型。另请注意 此子句中给出的一些候选运算符函数比内置运算符函数更宽松 操作员本身。如 13.3.1.2 中所述,通过重载解析选择内置运算符后 表达式受第 5 条中给出的内置运算符要求的约束,因此 那里给出的任何其他语义约束。如果有同名的用户编写的候选项,则 和参数类型作为内置候选运算符函数,内置运算符函数是隐藏的和 不包含在候选函数集中。— 尾注 ]

对于每个 cv 限定或非 cv 限定对象类型 T,都存在以下形式的候选运算符函数

T& operator[](T *, std::p trdiff_t(;

T& operator[](std::p trdiff_t, T *(;

编辑

一旦你超越了存在哪些运算符函数,这就会成为标准的重载解决方案,如标准第 13.3 节所述——大约 10 页的细节,但它的要点是,为了使函数调用不模棱两可,需要有一个至少尽可能匹配的函数, 每个参数上的可行函数,并且至少一个参数上的匹配比其他函数更好。 关于"更好"的确切含义有很多规范细节,但它归结为(在这种情况下(不需要任何用户定义的转换运算符或对象构造函数的匹配比需要的匹配更好。

因此,在这种情况下,有两个可行的匹配项:

void Something::operator[](const foo& f)
operator[](long, const char *)
第一个参数与第一个参数匹配

,而第二个参数与第二个参数匹配。 因此,除非有其他功能比这两个功能更好,否则它是模棱两可的。

后一点是一个可能的解决方法 - 添加:

void operator[](const char *a) { return (*this)[foo(a)]; }

到类 某物