【问题标题】:gcc *sometimes* resolves overload ambiguity in a strange waygcc *sometimes* 以一种奇怪的方式解决了重载歧义
【发布时间】:2014-05-02 18:57:48
【问题描述】:

在回答之前:这不是关于如何让这段代码做我想做的事情的问题。我已经知道该怎么做(见这个问题的结尾)。这是一个关于理解编译器为什么会这样做的问题。

请考虑以下(简化的)代码:

#include <iostream>

void operator>>( std::istream &stream, char chr )
{
    std::cout<<"Called "<<chr<<"\n";
}

int main()
{
    char c='b';
    std::cin>>c;

    return 0;
}

使用 gcc 4.8.2 和 -Wall -Wextra 编译会产生一个不相关的警告(未使用流)。但是,运行时会打印“Called b”。

我预计会发生两件事之一。要么程序从标准输入中读取一个字符,要么代码无法编译,因为编译器发现我的运算符和标准库定义的运算符不明确。标准库定义的算子,据我理解,相当于:

std::istream &operator>>( std::istream &stream, char &c )

无论如何,我没想到会调用我的操作员。

更奇怪的是,由于上面提到的歧义,以下代码无法编译:

#include <iostream>

void function(char &chr)
{
    std::cout<<"1 "<<chr<<"\n";
}

void function(char chr)
{
    std::cout<<"2 "<<chr<<"\n";
}

int main()
{
    char c='a';
    function(c);

    return 0;
}

除了使用函数而不是运算符之外,我认为编译器对这两种情况的决定没有任何区别。

注意:我完全意识到标准库定义的和我上面写的原型并不完全相同。我认为,在解决函数方面,至少在这个用例中,没有什么重要的区别。请记住,std 定义不能有默认参数,因为这是运算符重载。

另外,正如我在问题开头所写的,我已经知道如何让代码做正确的事。用“const char &”而不是“char”定义我的运算符会导致编译器选择正确的重载。

【问题讨论】:

  • 嗯,这可能与您的过载采用istreamstream 更具体的事实有关吗?我想我记得在标准中读到,在编译器构建的集合中的重载之间进行选择时,实际上会选择具有最具体参数类型的函数。疯狂的猜测,因为我不确定......
  • 你的输入操作符会导致undefined behavior,它应该返回你传入的流。如果你只收到一个警告,你真的应该启用更多警告(至少-Wall)。跨度>
  • 至于被调用,记住 C++ 是基于 arguments 而不是返回类型进行重载的。
  • @JoachimPileborg 返回类型为void
  • @JoachimPileborg 但是 OP 不链接该运算符。因此没有编译器错误,也没有由于没有从函数返回任何内容而导致的 UB。

标签: c++ gcc


【解决方案1】:

对于调用std::cin&gt;&gt;c;,有两个可行的函数。你声明的那个

void operator>>( std::istream &stream, char chr );

以及在流库中定义的那个(作为函数模板的特化):

template< class CharT, class Traits >
basic_istream<CharT,Traits>& operator>>( basic_istream<CharT,Traits>& st, CharT& ch );

按照13.3.3 [over.match.best] 中的描述选择最终赢得重载解决方案的最佳可行函数:

1 如下定义ICSi(F):

——如果 F 是静态成员函数,ICS1 (F) 被定义为 ICS1 (F) 既不比 ICS1 好也不差 (G) 对于任何函数 G,并且对称地,ICS1 (G) 都不是更好 不低于 ICS1 (F);否则,

——让 ICSi(F) 表示 隐式转换序列,将第 i 个参数转换为 列出可行函数 F 的第 i 个参数的类型。 13.3.3.1 定义隐式转换序列和 13.3.3.2 定义了一个隐式转换序列比它更好或更差的转换序列意味着什么 另一个。

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

——对于某些参数 j,ICSj(F1) 是一个比 ICSj(F2),或者,如果不是,

——上下文是一个初始化 用户定义的转换(见 8.5、13.3.1.5 和 13.3.1.6)和 从 F1 的返回类型到 目标类型(即,正在初始化的实体的类型)是 比标准转换序列更好的转换序列 F2 的返回类型到目标类型。
[示例:省略]或者,如果不是,

F1是一个非模板函数 F2 是一个函数模板特化,或者,如果不是,

— F1 和 F2 是函数模板特化,函数 F1 的模板比 F2 的模板更专业 到 14.5.6.2 中描述的部分排序规则。

2 如果只有一个可行的函数是更好的函数 比所有其他可行的功能,那么它是由 重载决议;否则调用格式不正确。

为了简化 - 在其他条件相同的情况下,非模板比模板特化更好。

function 的示例中,候选人之间没有这种区别。调用所需的两个隐式转换序列(char->char&amp;char->char)都是身份转换并且是模棱两可的。

【讨论】:

  • 所以,如果我正确理解了您的答案,这与“const char &chr”一起工作的原因是因为“对于某些参数 j,ICSj(F1) 是比 ICSj(F2) 更好的转换序列“?
  • @ShacharShemesh 不,这仍然模棱两可。 char->const char&amp; 是完全匹配的。
  • 如果这是真的,那么基于引用的恒定性的重载将永远不会起作用。另外,您是否建议 gcc 编译它是错误的?
  • @ShacharShemesh 还有其他规则可以对隐式转换进行排序。例如来自 13.3.3.2:S1 is a better conversion sequence than S2 if [...] — S1 and S2 are reference bindings (8.5.3), and the types to which the references refer are the same type except for top-level cv-qualifiers, and the type to which the reference initialized by S2 refers is more cv-qualified than the type to which the reference initialized by S1 refers.
  • @ShacharShemesh 我可能误解了。 const char&amp;,你的意思是你改变了functionoperator&gt;&gt;的签名?
【解决方案2】:

标准operator &gt;&gt; 是一个模板。非模板胜过模板 (13.3.3/1)只要参数转换具有相同的质量 (13.3.3.2/3)(还有其他条件,但此处不适用)。

由于char lvalue -&gt; charchar lvalue -&gt; char&amp; 都不比另一个更好,因此非模板获胜。

如果你用const char&amp; 声明一个非模板,那么它会比标准的&gt;&gt; 更糟糕,因为const char&amp;char&amp; 有更多的限定符。所以std::operator&gt;&gt; 获胜。

还有另一个原因:Koenig 查找。标准operator&gt;&gt; 是命名空间std 的成员,只有在“正常”查找失败时才能找到它。如果您使用了using namespace std,则 Koenig 查找将不是一个因素。 上述内容不正确。

【讨论】:

  • 具体来说,字符类型的operator&gt;&gt; 是模板。其他一些不是(而是成员函数)。
  • 它的原型准确地说是template&lt;class charT, class traits&gt; basic_istream&lt;charT,traits&gt;&amp; operator&gt;&gt;(basic_istream&lt;charT,traits&gt;&amp;, charT&amp;);
  • 非模板并不总是胜过模板。如果是这样,那么void f(...) { } template &lt;typename T&gt; void f(T); int main() { f(3); } 将编译并运行并且什么都不做。它会编译,但不会链接,因为它会尝试调用未定义的f&lt;int&gt;
  • 你的另一个理由同样是假的。如果它是正确的,那么namespace N { struct S { } s; void operator&lt;&lt;(S, int); } void operator&lt;&lt;(N::S, long) { } int main() { N::s &lt;&lt; 3; } 将编译并运行并且什么都不做。这也可以编译,但不会链接,因为它会找到 N::operator&lt;&lt;,即使还有一个 可以 被调用的 ::operator&lt;&lt;
  • -1 标准运算符>>是命名空间std的成员,只有在“正常”查找失败时才能找到它。这是完全错误的。
猜你喜欢
  • 2019-08-18
  • 2017-01-17
  • 2017-12-04
  • 1970-01-01
  • 1970-01-01
  • 2018-06-02
  • 1970-01-01
  • 1970-01-01
  • 2019-08-03
相关资源
最近更新 更多