当内部作用域不起作用时,为啥编译器不采用命名空间名称?

Posted

技术标签:

【中文标题】当内部作用域不起作用时,为啥编译器不采用命名空间名称?【英文标题】:Why don't the compiler take the namespace name when the inner scope ones don't work?当内部作用域不起作用时,为什么编译器不采用命名空间名称? 【发布时间】:2013-05-22 16:26:49 【问题描述】:

我认为我非常了解名称查找(在观看了几个有关它的视频并阅读了很多内容之后),但我只是遇到了这种情况:

#include <iostream>

namespace test

  struct Id
  ;

  void do_something( const Id& )  std::cout << "Hello, World!" << std::endl; 

  class Test
  
  public:

    void do_something()  std::cout << "WTF!" << std::endl;  

    void run()
    
      Id id;
      do_something( id ); // doesn't compile
    

  ;



int main()

    test::Test my_test;
  my_test.run();

指出的行不编译(在 GCC4.8 和 VC11U2 上),因为它尝试使用成员函数 test::Test::do_something() 而不是名称空间范围的 test::do_something( const Id&amp; ),这似乎是唯一可能的候选者。

显然,成员函数名称隐藏了命名空间范围的名称,这让我感到惊讶,因为我记得在其他上下文中使用几乎相似的代码而没有产生此问题(但最终条件可能会非常不同)。

我的问题是:这些编译器是否符合标准?

(不幸的是,通过阅读标准文档很难理解名称查找,所以我需要专家确认)

【问题讨论】:

不,不。名称查找很容易。只是a simple little formula. do_something 不是全局的;它位于 test 命名空间中。 @chepner Truem 让我解决这个问题。 【参考方案1】:

我的问题是:这些编译器是否符合标准?

是的。在重载决议决定哪些函数是可行候选函数(包括检查参数的数量)之前,编译器首先必须进行名称查找,以找到所有候选函数,可行的和不可行的。在您的示例中,名称查找在找到成员 do_something() 后停止,因此重载决议永远没有机会确定命名空间范围是否可行。

3.4.1 [basic.lookup.unqual]/1:“在 3.4.1 中列出的所有情况下,将按照每个相应类别中列出的顺序搜索范围以查找声明;名称查找会尽快结束作为名称的声明。”

3.4.1 [basic.lookup.unqual] 第 8 段列出了搜索名称的上下文,甚至还有一个示例可以准确回答您的问题。 Test 的范围在封闭的命名空间之前被搜索,并且正如第 1 段所说,“一旦找到名称的声明,名称查找就结束”

【讨论】:

【参考方案2】:

一门语言越是努力为一个结构找到一个有效的解释,一个错字或其他类似错误就越有可能导致编译器找到一个有效但错误的含义。编译器假设如果foo 在某个范围内定义,并且该范围内的代码使用foo,则程序员打算让代码使用在该范围内定义的foo。如果程序员试图用 foo 做一些其内部范围定义不允许的事情,那么很有可能出现以下情况之一:

程序员打算做一些稍微不同的操作,这在内部 foo 上是有效的。除非程序员指定编译器,否则不能期望编译器知道程序员的意图。 程序员打算执行指定的操作,但没有意识到内部foo 不能支持它,因此必须找到内部foo 可以支持的一些其他操作或操作序列。同样,除非程序员指出如何正确使用foo,否则不能期望编译器生成好的代码。 程序员打算对外部 foo 执行相关操作,但拒绝明确表示。如果编译器想猜测这就是程序员的意思,它可以生成具有这种行为的代码。

只有当程序员的意图是#3 时,编译器才可能生成行为符合预期的代码。然而,更有可能的是,程序员真正想要的是#1 或#2。如果编译器即使假设#3 会产生有效 代码也拒绝编译代码,那么将发现上述任何错误并因此可以被纠正。相比之下,如果编译器尽可能假设 #3,那么如果程序员真的打算 #1 或 #2 问题,直到代码运行并表现出与设计相反的行为才会显现出来。

顺便说一句,如果我有我的 druthers,我会将此原则应用于 .NET 语言中的区分大小写,不仅禁止以与定义不一致的方式写入任何标识符(如 C# 所做的,但 vb. net),但使用的任何标识符仅在大写/小写方面与内部范围内的标识符不同。例如:

class foo

  int x;
  void bar()
  
    int X=2;
    x=4; // ****
    return X;
  

鉴于上面的代码,C# 会猜测带星号的行是用来写字段的;给定类似的代码,vb.net 会假设它打算编写局部变量。就个人而言,我不喜欢这两种假设。 “准确地说出你的意思”的原则向我建议,编译器应该要求程序员说 this.x=4;X=4;,这两者都不能被解读为具有错误的含义。

【讨论】:

+1,虽然我不同意在任何地方使用this。这是一个打击初学者的错误,而不是可能导致现实世界应用程序混乱的错误。此外,编译器会发出警告。 @EdS.:我需要this 的唯一“额外”时间是在存在名称相同的局部变量的情况下,除了大写/小写。顺便说一句,我真的不喜欢流行的 C# 约定,其中属性Foo 的私​​有支持字段称为foo;我认为这比_foo_Foo 没有任何优势。在我看来,前导下划线比小写首字母的后者更具视觉特色,并且在不少情况下,作为字段名称与属性名称不同,它是合法的。 Ad 3) 我认为这并不总是可能的,例如如果函数的参数类型是模板参数。 @supercat:哦,那我想我误解了你,我们完全同意:D。出于同样的原因,我也不喜欢仅使用小写字母的支持字段;它们到处都与方法参数发生冲突。 @EdS.:很多想要区分大小写的人主要是想让编译器在标识符的大小写错误时发出尖叫声,而许多想要不区分大小写的人主要是想让编译器在有错误的情况下发出声音仅按大小写不同的标识符。为什么不让这两组人都开心,并要求标识符正确大小写,但也不止大小写不同。追溯实施这样的规则为时已晚,但我认为它会提供两全其美的效果。【参考方案3】:

这根本不是查找问题。关键是lookup在重载决议启动之前完成。当编译器看到do_something时,它会执行lookup来弄清楚它的含义,它发现它是一个函数,这反过来又激活了ADL寻找其他潜在的过载。然后查找完成并开始重载解析。当重载解决失败时。

【讨论】:

【参考方案4】:

编译器将收集所有“候选名称”,除非涉及 ADL,否则它们将来自同一范围,然后尝试选择最佳匹配(如果可用)。在任何情况下,失败的匹配都不会导致它尝试从备用范围中查找其他候选名称。

这与编译器首先进行重载解析然后检查成员的公共/私有以查看它是否实际可访问的方式非常相似。

g++ 有一个方便的-Wshadow 选项来寻找阴影(不过我不确定它是否会特别警告这一点)。

【讨论】:

[basic.lookup.unqual] “一旦找到名称的声明,名称查找就会结束。”它真的收集了所有个名字吗? @Klaim:命名空间函数的问题,更适合窃取您的成员调用? @Xeo 好点,但为什么不使用明确的this-&gt; 来解决这个问题 - 这比找到定义命名空间范围函数的命名空间并使用限定 ID 更简单 IMO ? "但是为什么不用显式的 this->" 来解决这个问题,那么你必须用this-&gt; 编写每个成员函数,即使当前没有窃取查找的外部函数。否则将来对外部代码的更改可能会改变代码的含义。 @DyP 可以到处写this-&gt;。规则 “禁止将泛型非成员函数编写为类成员的泛型代码”,如果您的意思是包含非成员名称,则输入using test::do_something; 否则它应该找到“最近”的名称(即从当前范围开始并向外工作到封闭范围)【参考方案5】:

不幸的是,通过阅读标准文档很难理解名称查找,所以我需要专家确认

无论如何我都不是专家,但我是这样理解标准的名称查找规则的。

两个例子:

void foo(int);

namespace associated

    struct bee ;
    void flower(bee);


namespace bar

    void foo();
    void flower();

    void test()
    
        foo(42);                   // (A)
        flower(associated::bee()); // (B)
    


int main()

    bar::test();

(A) 无法编译,因为 [basic.lookup.unqual]: "name lookup 在找到该名称的声明后立即结束"

(B) 确实编译,因为 ADL; associated 是一个关联的命名空间。

但是,有 [basic.lookup.argdep]/3:

令 X 为非限定查找 (3.4.1) 生成的查找集,让 Y 为参数相关查找生成的查找集(定义如下)。如果 X 包含

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

那么 Y 为空。否则 Y 是在与参数类型关联的命名空间中找到的一组声明,如下所述。通过名称查找找到的声明集是 X 和 Y 的并集。

第一点适用于您的示例。因此,我认为是的,拒绝您的示例的编译器符合标准。

【讨论】:

这就是为什么你经常会发现“使用 std::swap;”在类成员交换函数的实现中(除了将交换纳入基本类型的范围,如int ofc)。防止查找会阻止 ADL 工作的成员交换函数。 @JohannesSchaub-litb 有没有办法让在命名空间依赖于模板类型参数的命名空间中声明的函数的声明可见?

以上是关于当内部作用域不起作用时,为啥编译器不采用命名空间名称?的主要内容,如果未能解决你的问题,请参考以下文章

为啥使用命名空间在我的 C++ 项目中不起作用?

为什么命名空间重载不起作用?

为啥 Azure 事件中心订阅者不起作用?

命名空间和作用域

python之函数命名空间和作用域

命名空间和作用域