19

这个例子似乎可以用 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;
}

但是clang抱怨:

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"]为 as d.operator[]("32"),那么重载决议甚至不会查看选项 2,并且 clang 也将编译得很好。

编辑:(澄清问题)

这似乎是重载解决方案中的一个复杂领域,因此我非常感谢在这种情况下详细解释重载解决方案并引用标准的答案(如果有一些模糊/高级可能是未知的规则) .

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

注意:如果将 operator double() 更改为 operator bool(),所有三个 (clang, vc, gcc) 将拒绝编译,并出现类似的模棱两可的错误。

4

3 回答 3

12

通过逐步了解重载解决方案,应该更容易理解为什么重载解决方案是模棱两可的。

§13.5.5 [over.sub]

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

现在,我们首先需要一个重载集。这是根据§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)如下:

  • ifF是静态成员函数,[...]; 除此以外,
  • let表示将列表中的第 - 个参数转换为可行函数的第 - 个参数的类型ICSi(F)的隐式转换序列。13.3.3.1 定义了隐式转换序列,13.3.3.2 定义了一个隐式转换序列比另一个更好或更差的转换序列意味着什么。iiF

现在让我们为重载集 ( ) 构造那些隐式f1转换f2序列§13.3.3.1

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 拒绝该代码是正确的。

于 2012-01-18T20:42:27.743 回答
4

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

  • 对于Something::operator[](const foo& f):(1-1)身份转换,和(1-2)foo("32")通过foo::foo(const char*)
  • for 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)中的定义,

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

因此,所考虑的两个候选函数中的任何一个都不比另一个更好,因此该调用是非良构的。即 Clang 似乎是正确的,并且代码不应该编译。

于 2012-01-18T19:51:43.157 回答
3

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

13.6 内置运算符 [over.built]

本节规定了代表第 5 章中定义的内置运算符的候选运算符函数。这些候选函数参与 13.3.1.2 中描述的运算符重载决策过程,并且不用于其他目的。[ 注意:因为内置运算符只接受非类类型的操作数,并且运算符重载决策仅在操作数表达式最初具有类或枚举类型时才会发生,因此运算符重载决策只有在操作数具有具有用户定义转换为适合运算符的非类类型的类类型,或者当操作数具有可以转换为适合运算符的类型的枚举类型时。另请注意,本小节中给出的一些候选运算符函数比内置运算符本身更宽松。如 13.3.1.2 中所述,在通过重载决议选择内置运算符后,表达式受第 5 章中给出的内置运算符的要求的约束,因此受制于此处给出的任何附加语义约束。如果存在与内置候选算子函数具有相同名称和参数类型的用户编写的候选函数,则该内置算子函数被隐藏并且不包含在候选函数集中。——尾注] 在通过重载决议选择内置运算符后,表达式受第 5 章中给出的内置运算符的要求的约束,因此受制于此处给出的任何附加语义约束。如果存在与内置候选算子函数具有相同名称和参数类型的用户编写的候选函数,则该内置算子函数被隐藏并且不包含在候选函数集中。——尾注] 在通过重载决议选择内置运算符后,表达式受第 5 章中给出的内置运算符的要求的约束,因此受制于此处给出的任何附加语义约束。如果存在与内置候选算子函数具有相同名称和参数类型的用户编写的候选函数,则该内置算子函数被隐藏并且不包含在候选函数集中。——尾注]

对于每个 cv-qualified 或 cv-unqualified 对象类型 T 存在以下形式的候选运算符函数

T& 运算符[](T *, std::ptrdiff_t);

T& 运算符[](std::ptrdiff_t, T *);

编辑

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

所以在这种情况下,有两个可行的匹配:

void Something::operator[](const foo& f)
operator[](long, const char *)

第一个更适合第一个参数,而第二个更适合第二个参数。因此,除非有其他功能比这两者都好,否则它是模棱两可的。

后一点是一种可能的解决方法——添加:

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

上课的东西

于 2012-01-18T18:59:20.907 回答