C ++使用指令如何工作?

2020-12-24 21:18:16

使用声明在语义上类似于声明;它在当前范围内引入了名称的新含义。如果该名称已经具有来自外部作用域的一个或多个含义,则这些现有的含义将被新声明隐藏或遮盖。 Godbolt:

命名空间AnimalUtils {int foo(Zoo :: Animal);}命名空间Outer {int foo(Zoo :: Lion);命名空间Inner {int foo(Zoo :: Cat); //声明隐藏了Outer :: foo int test1(){return foo(Zoo :: Lion()); } int test2(){使用AnimalUtils :: foo; // using-declaration隐藏Inner :: foo return foo(Zoo :: Lion()); //调用AnimalUtils :: foo}}}

Inner中foo的声明隐藏或覆盖了foo之前的含义;因此,当我们在test1中查找名称foo时,我们仅发现Inner :: foo。甚至不将Outer :: foo视为候选人。

在test2中对foo的使用声明隐藏或遮盖了foohad的先前含义;因此,当我们在test2中查找名称foo时,只会发现AnimalUtils :: foo。我们甚至都没有考虑将Inner :: foo作为候选对象。

(所以std :: swap两步工作如何工作?那么,除了正常的不合格查找外,我们还进行单独的参数依赖查找,并合并两个候选集。因此,在这种特殊情况下,重载解析也将考虑任何在名称空间Zoo中声明的foo的含义。但是此示例中没有这样的foo。)

using指令比using声明更巧妙。但是,好消息是您永远不要使用它们。严重的是,永远不要使用命名空间进行写操作。这样您就不会遇到任何麻烦。只是假装它们不存在。

使用指令还将名称的新含义引入范围。但是令人惊讶的是,他们没有将这些含义引入当前的范围! using指令将其新含义引入范围,该范围是当前范围和目标名称空间自己的范围的最低共同祖先。

在有向无环图(DAG)中,可以通过追踪从A到根的路径,再追踪从B到根的路径来找到两个节点A和B的“最低共同祖先” —最低共同祖先是两条路径首先汇合的地方。

或者,从根向下开始跟踪“完全限定的路径”;最低的公共祖先对应于这些完全限定名称中的最长的公共前缀。例如,考虑以下名称空间层次结构:

名称空间NA {}名称空间NB {名称空间NC {名称空间N1 {}名称空间N2 {}}}

NB :: NC :: N1和NB :: NC :: N2的最低公共祖先是NB :: NC.NA和NB :: NC :: N2的最低公共祖先是根(全局命名空间)。

命名空间NA {int foo(Zoo :: Lion);}命名空间NB {int foo(Zoo :: Lion);名称空间NC {名称空间N1 {int foo(Zoo :: Cat); }命名空间N2 {int test(){使用命名空间N1;使用名称空间NA;返回foo(Zoo :: Lion()); }}}}

您可能有理由期望,当测试将Lion传递给foo时,它将导致对NA :: foo(Lion)的调用,因为我们“使用指令的”命名空间NA。或至少它可以称为NB :: foo(Lion),因为在完全没有任何使用指令的情况下会发生这种情况。但实际上,它称为N1 :: foo(Cat)!

如下图所示:使用命名空间N1导致将N1 :: foo(Cat)声明注入到N1和N2的最小公祖中,即NC。此名称foo的声明隐藏了在更高范围内引入的foo的所有含义,例如NB :: foo(Lion)。

使用命名空间NA会导致将NA :: foo(Lion)的声明注入到NA和N2的最小公共祖先中,这是全局命名空间。此注入的声明最终被NB :: foo(Lion)的声明隐藏。 ,而这又被注入到命名空间NC中的N1 :: foo(Cat)声明所隐藏。

顺便说一句,即使N1 :: foo的声明已经注入到NC中,您实际上也不能将其称为NC :: foo。它仅出于不合格查找的目的而注入,并且仅出于在此范围内发生的不合格查找的目的而注入。测试范围之外的任何人都不会“看到”将foo声明注入名称空间NC的声明。

命名空间详细信息{struct Impl {}; void swap(Impl&amp ;, Impl&);模板< T类> void example(T& x,T& y){使用命名空间std; swap(x,y); }}

不好的原因有三个。第一,它无法使用隐藏的朋友习惯用法进行交换(Impl&,Impl&)。第二,它使用命名空间std。 (这两个标记都应该使您进行的任何代码检查都无法通过。)第三,就“ std :: swap两步操作”而言,使用命名空间是错误的。(请参阅“什么是std: :swap two-step?”(2020-07-11)。)在这两个步骤中,我们希望将名称空间std的swap的含义带入当前作用域,以便swap(x,y)会考虑该含义。除了ADL分配的任何含义。但是,我们要做的是将交换(以及std中的其他名称)的含义从std名称空间引入到std和detail的最不常见的祖先名称空间中;即进入全局名称空间。

因此,当我们尝试实例化example< int>时,不合格的查找将在我们当前作用域中向外寻找交换的声明,并查找detail :: swap(Impl&amp ;, Impl&)并在那里停止。它永远找不到在全局名称空间中声明的模板化std :: swap。糟糕!这就是为什么正确的std :: swap两步版本使用using-声明而不是using-directive的原因。

名称空间ND :: N3 {int foo(Zoo :: Cat);}名称空间NE {使用名称空间ND :: N3; int foo(Zoo :: Lion);}命名空间ND {命名空间N4 {int test3(){使用命名空间NE;返回foo(Zoo :: Lion()); }}}

使用指令是“传递”的;由于NE显然包含ND的using指令,因此test3似乎同时包含使用命名空间NE和使用命名空间ND :: N3。

在这个人为的示例中,来自NE的名称被注入到ND :: N4和NE的最不常见祖先(即全局名称空间)中。将ND :: N3中的名称注入ND :: N4和ND :: N3(即ND)的最小公共祖先中。然后,foo的unqualifiedlookup从ND :: N4 :: test3向外扫描,并在找到已注入命名空间ND的ND :: N3 :: foo(Cat)声明后立即停止。该声明隐藏了表面上更好匹配的NE :: foo(Lion),它被注入到全局名称空间中。

请注意,using指令的“传递性”仅适用于using命名空间中直接存在的using指令。如果test3说使用命名空间NE :: N5,它将从NE :: N5而不是从其父命名空间NE(而不是从全局命名空间)中选择使用指令。

以及所有这些如何与内联名称空间交互?好吧,让我们再保留一天。

其中一些示例可能会让您感到惊讶。回顾一下,我希望这些解释还是有意义的,但是这些信息对优秀的C ++程序员有用吗?我不得不说不,不是。好的C ++程序员不要使用using指令-我希望这篇博客文章已说服您加入他们!

至于为什么使用指令以这种奇怪的,不直观的方式工作,我已经在Stack Overflow上询问了这个问题。因此,如果您有一个好的答案,那是输入它的好地方!