【问题标题】:confusion about C++ name lookup关于 C++ 名称查找的困惑
【发布时间】:2021-05-08 20:02:54
【问题描述】:

据我所知,嵌套范围内的名称将在封闭范围内隐藏相同的名称,如下所示:

namespace ttt {
    class A {};

    void test(const A&, int)
    {
        cout << "ttt::test()" << endl;
    }
}

void test(const ttt::A&, int)
{
    cout << "global::test()" << endl;
}

int main()
{
    void test(const ttt::A&, int);
    ttt::A a;
    test(a, 1);
}

主函数中void test(const ttt::A&amp;, int); 的声明隐藏了与命名空间ttt 中相同的名称,因此控制台打印global::test()(在Visual Studio 2019 中测试)

但是,当我尝试下面的代码时:

std::ostream& operator<< (std::ostream& os, const string& str)
{
    os << "global::operator" << endl;
    return os;
}

int main()
{
    std::ostream& operator<< (std::ostream & os, const string & str);
    string a = "STD's operator";

    cout << a << "STD's operator" << endl;
}

我尝试用我自己的&lt;&lt; 版本重载&lt;&lt; 运算符,它是在STL 中定义的模板。根据第一个例子,main中operator&lt;&lt;的声明应该隐藏&lt;&lt;的STL定义版本,那么期望的输出应该是

global::operator
global::operator
global::operator

或者编译错误,因为我不知道endl是否可以转换为string。 但是,程序的结果是:

global::operator
STD's operator

所以cout &lt;&lt; a &lt;&lt; "STD's operator" &lt;&lt; endl; 语句中的第二个也是最后一个&lt;&lt; 调用了STL 的&lt;&lt;,而不是我定义的重载。 &lt;&lt; 不应该已经被 main 中的声明 std::ostream&amp; operator&lt;&lt; (std::ostream &amp; os, const string &amp; str); 隐藏了吗?

有人可能会说"STD's operator"const char*,因此Argument Dependent Lookup(ADL) 从std 命名空间添加了一个更好的候选对象std::ostream&amp; operator&lt;&lt; (std::ostream&amp;, const char*)。如果这是真的,那么如何解释第一个例子。第一个示例中的 ADL 过程可能会将 ttt::test(const A&amp;, int) 添加到重载候选中,这会导致第一个示例中的歧义,但这并没有发生,ttt::test(const A&amp;, int) 只是被隐藏了。

“C++ Primer 5th”的第 798 页说“当我们将类类型的对象传递给函数时,编译器会搜索定义参数类的命名空间除了正常范围查找”。我认为我的困惑是关于除了的准确含义。 如果这意味着类的命名空间与调用函数的范围具有相同的优先级,那么第一个示例应该会引起歧义。 如果这意味着该类的命名空间具有较低的优先级,那么第二个示例中main函数中的所有&lt;&lt;应该被我定义的版本隐藏。 如果这意味着类的命名空间具有更高的优先级,那么第一个示例应该打印"ttt::test()"。 那么发生了什么?

【问题讨论】:

  • "STD's operator" 不是std::string,而是const char[N]
  • @NathanOliver 嘿家伙,我更新了我的问题。
  • 完整阅读一本 C++ 教科书,不要在第 3 章之后停下来。

标签: c++ c++11 namespaces


【解决方案1】:

如果候选函数之一在块范围内声明,则执行 ADL,就像您的块范围函数声明一样。 (在执行ADL时,在挑选可行的候选者时确实没有特殊的优待)

所以你的第一个例子,void test(const ttt::A&amp;, int);(这是 ::test 的块范围声明),意味着 ttt::test 不再是候选对象,并且全局 ::test 被调用(删除块范围声明使模棱两可)


我相信您在这里是正确的,第二个示例应该在构造 std::string 之后调用您的全局运算符。

编译器似乎同意std::cout &lt;&lt; a &lt;&lt; "STD's operator" &lt;&lt; std::endl 编译为等价于std::operator&lt;&lt;(::operator&lt;&lt;(std::cout, a), "STD's operator").operator&lt;&lt;(std::endl)&lt;&lt; std::endl 很好,因为它是通过成员查找而不是 ADL 找到的。问题是还是用ADL找std::operator&lt;&lt;(std::ostream&amp;, const char*)

直接从 C++11 标准中阅读应该发生的事情:

“表达式中的运算符” [over.match.oper]p2:

如果任一操作数的类型是类或枚举,则可能会声明一个用户定义的运算符函数来实现此运算符 [...]。在这种情况下,重载决策用于确定要调用哪个运算符函数或内置运算符来实现运算符。因此,运算符符号首先转换为表 11 中总结的等效函数调用符号(其中 @ 表示指定子类中涵盖的运算符之一)

两个操作数都是类类型,所以用户定义的操作符函数被认为是预期的。表11的相关子条款是13.5.2,表达式a@b,即(a).operator@(b)作为成员函数,operator@(a, b)作为非成员函数。

[over.match.oper]p3

[...] 用于二元运算符 @,左操作数为 cv1 T1,右操作数为 cv2 T2,三组候选函数,指定的成员候选非成员候选内置候选,构造如下:
[...]
非成员候选集是根据非限定函数调用 (3.4.2) 中的通常查找,在表达式上下文中对 operator@ 进行非限定查找的结果,除了忽略所有成员函数。

其中 §3.4.2 是 [basic.lookup.arg] “参数相关名称查找”。

[basic.lookup.arg]p3 说:

X 为非限定查找(3.4.1)产生的查找集,令Y 为参数依赖查找产生的查找集(定义如下)。如果 X 包含

  • 类成员的声明,或
  • 不是using-declaration的块范围声明,或
  • 既不是函数也不是函数模板的声明

然后 Y 为空。 [...] 通过名称查找找到的声明集是 XY 的并集。

为简单起见,仅查看std::cout &lt;&lt; "STD's operator",查找名称为operator&lt;&lt;。非限定查找仅找到 std::ostream&amp; operator&lt;&lt; (std::ostream &amp; os, const string &amp; str); 的块范围声明(并且所有其他声明都被隐藏)。但是由于找到了块作用域中的函数声明,因此不应该发生 ADL,并且没有更多的非成员候选者。

因此候选函数集只有全局::operator&lt;&lt;(std::ostream &amp; os, const string &amp;)std::ostream及其基类中的成员operator&lt;&lt;,其中全局函数是最可行的。

编译器在查找运算符时似乎忽略了这条规则,即使有块范围声明,也总是执行 ADL。写入operator&lt;&lt;(std::cout, "STD's operator") 正确并输出global::operator

【讨论】:

  • "当函数调用 ([expr.call]) 中的后缀表达式是非限定 ID 时" cout &lt;&lt; a &lt;&lt; "STD's operator" &lt;&lt; endl 中的任何内容都不是函数中的后缀表达式调用表达式。也没有实际的函数调用表达式。
  • @StoryTeller-UnslanderMonica 改写为operator&lt;&lt;(operator&lt;&lt;(cout, a), "STD's operator")的形式是
  • 您引用的 C++20 文本与 up to C++17 的措辞不同。它没有明确提到 [basic.lookup.unqual]。 OP 明确标记了这个 C++11。因此,这使我相信,除非此更改是缺陷报告的一部分,并且追溯适用,否则编译器供应商可以原谅没有在 c++20 之前的模式下进行常规的非限定名称查找。跳过这一点,我们就没有任何东西可以隐藏 ADL 的结果了。
  • @StoryTeller-UnslanderMonica 在 C++17 中有类似的措辞,如果存在块范围函数声明,则不使用 ADL 结果:timsong-cpp.github.io/cppwp/n4659/basic.lookup.argdep#3 这将适用于“通常的规则C++17 中引用的非限定函数调用中的名称查找”(如果选择了非成员函数,似乎标准打算让 (a) &lt;&lt; (b) 完全等同于 operator&lt;&lt;(a, b),因此应该应用相同的查找规则) .虽然是的,但我没有看到 C++11 标记并将换掉引号
  • [basic.lookup.argdep]/3 不仅相似,标准之间也一样。这与我的观点并不矛盾。它取决于已执行的非限定查找。我的观点是,供应商可能了解将跳过不合格的查找。所以X 会是空的。反正那是没有意义的。 GCC 和 Clang 对于不是来自 std (wandbox.org/permlink/SRQRl5ne0YYTEsai) 的类型都表现不正确,他们看到了歧义。 OP 中唯一剩下的兴趣点是endl。不应隐藏该重载,因为它是一个成员函数。
【解决方案2】:

仅在@Artyer 的回答之外。

实际上,您永远不应该在不同的命名空间中提供不同的std:: 函数定义,因为调用可能会意外进入std:: 版本,而不会发出警告或错误。

稳健的解决方案是为您的函数命名或将您的参数包装在不同的类对象中,这样您就不会在不打算调用时意外调用 std:: 函数。

例如,如果您想要一个可以与-ffast-math 正常工作的std::isnan 版本,请将其命名为isnan2,而不是可能解析为std::isnanisnan

成为一名优秀的软件工程师的一部分是预防你自己未来的潜在问题。避免与可能通过 ADL 找到的 std:: 函数的名称冲突是鲁棒工程 - 一种关于工程的哲学或思考方式,它使现有正常工作代码的意外语义更改成为不可能。

在您的示例中,没有必要也没有理由在另一个命名空间中创建另一个版本的 std::operator&lt;&lt;。但是这样的决定会产生一些不希望的问题。也就是说,回报是0,但风险是无限的。

【讨论】:

    猜你喜欢
    • 2020-08-06
    • 1970-01-01
    • 2013-08-11
    • 1970-01-01
    • 2013-07-23
    • 2019-05-17
    • 2020-03-06
    • 2016-03-16
    • 2013-07-27
    相关资源
    最近更新 更多