【问题标题】:Unnamed/anonymous namespaces vs. static functions未命名/匿名命名空间与静态函数
【发布时间】:2010-09-14 08:16:40
【问题描述】:

C++ 的一个特性是能够创建未命名(匿名)命名空间,如下所示:

namespace {
    int cannotAccessOutsideThisFile() { ... }
} // namespace

您会认为这样的功能毫无用处——因为您无法指定命名空间的名称,所以无法从外部访问其中的任何内容。但是这些未命名的命名空间可以在创建它们的文件中访问,就好像你对它们有一个隐含的 using 子句一样。

我的问题是,为什么或什么时候这比使用静态函数更可取?还是它们本质上是做同一件事的两种方式?

【问题讨论】:

标签: c++ namespaces


【解决方案1】:

C++ 标准在第 7.3.1.1 节未命名命名空间,第 2 段:

static关键字的使用是 在 a 中声明对象时不推荐使用 命名空间范围,未命名的命名空间 提供了一个优越的选择。

静态仅适用于对象、函数和匿名联合的名称,不适用于类型声明。

编辑:

弃用 static 关键字(影响翻译单元中变量声明的可见性)的决定已被撤销 (ref)。在这种情况下,使用static 或未命名的namespace 本质上是两种做同样事情的方式。更多讨论请见this SO question。

未命名的namespace 仍然具有允许您定义本地翻译单元类型的优势。请参阅this SO 问题了解更多详情。

感谢 Mike Percy 让我注意到这一点。

【讨论】:

  • Head Geek 询问仅针对函数使用的静态关键字。应用于命名空间范围内声明的实体的静态关键字指定其内部链接。在匿名命名空间中声明的实体具有外部链接 (C++/3.5),但它保证存在于唯一命名的范围内。未命名命名空间的这种匿名性有效地隐藏了它的声明,使其只能从翻译单元内访问。后者的工作方式与 static 关键字相同。
  • 外联的缺点是什么?这会影响内联吗?
  • 那些在 C++ 设计委员会中说不推荐使用 static 关键字的人可能从未在大型现实世界系统中使用过巨大的 C 代码...(如果您立即看到静态关键字,但看不到匿名命名空间它包含许多带有大注释块的声明。)
  • 因为这个答案出现在谷歌上作为“c++ 匿名命名空间”的最高结果,应该注意的是,静态的使用不再被弃用。请参阅stackoverflow.com/questions/4726570/…open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1012 了解更多信息。
  • @ErikAronesty 这听起来不对。你有一个可重复的例子吗?从 C++11 开始 - 甚至在某些编译器之前 - 未命名的 namespaces 隐含地具有内部链接,因此应该没有区别。通过在 C++11 中将此作为要求解决了以前可能因措辞不当而引起的任何问题。
【解决方案2】:

将方法放入匿名命名空间可防止您意外违反One Definition Rule,让您不必担心将帮助方法命名为与您可能链接的其他方法相同。

而且,正如 luke 所指出的,匿名命名空间比静态成员更受标准的青睐。

【讨论】:

  • 我指的是静态独立函数(即文件范围函数),而不是静态成员函数。静态独立函数与未命名命名空间中的函数非常相似,因此是个问题。
  • 啊;好吧,ODR 仍然适用。编辑删除段落。
  • 据我所知,当静态函数在标题中定义并且该标题包含在多个翻译单元中时,它的 ODR 不起作用,对吧?在这种情况下,您会收到相同功能的多个副本
  • @Andy T:在包含标题的情况下,您并没有真正看到“多个定义”。预处理器负责处理它。除非需要研究预处理器生成的输出,否则对我来说这看起来相当奇特和罕见。还有一个很好的做法是在头文件中包含“守卫”,例如:“#ifndef SOME_GUARD - #define SOME_GUARD ...”这应该可以防止预处理器两次包含相同的头文件。
  • @NikitaVorontsov 守卫可能会阻止将相同的标头包含到同一个翻译单元中,但是它允许在不同的翻译单元中进行多个定义。这可能会导致“多个定义”链接器错误。
【解决方案3】:

在一种极端情况下,静态具有令人惊讶的效果(至少对我来说是这样)。 C++03 标准在 14.6.4.2/1 中声明:

对于依赖于模板参数的函数调用,如果函数名称是 unqualified-id 而不是 template-id,则使用通常的查找规则(3.4.1、3.4.2)除了:

  • 对于使用非限定名称查找 (3.4.1) 的查找部分,只能找到具有来自模板定义上下文的外部链接的函数声明。
  • 对于使用关联命名空间 (3.4.2) 的查找部分,只能找到在模板定义上下文或模板实例化上下文中找到的具有外部链接的函数声明。

...

下面的代码将调用foo(void*) 而不是foo(S const &),如您所料。

template <typename T>
int b1 (T const & t)
{
  foo(t);
}

namespace NS
{
  namespace
  {
    struct S
    {
    public:
      operator void * () const;
    };

    void foo (void*);
    static void foo (S const &);   // Not considered 14.6.4.2(b1)
  }

}

void b2()
{
  NS::S s;
  b1 (s);
}

这本身可能没什么大不了的,但它确实强调了对于完全兼容的 C++ 编译器(即支持 export 的编译器),static 关键字仍然具有任何功能都不可用的功能其他方式。

// bar.h
export template <typename T>
int b1 (T const & t);

// bar.cc
#include "bar.h"
template <typename T>
int b1 (T const & t)
{
  foo(t);
}

// foo.cc
#include "bar.h"
namespace NS
{
  namespace
  {
    struct S
    {
    };

    void foo (S const & s);  // Will be found by different TU 'bar.cc'
  }
}

void b2()
{
  NS::S s;
  b1 (s);
}

确保在使用 ADL 的模板中找不到我们未命名命名空间中的函数的唯一方法是将其设为 static

现代 C++ 更新

从 C++ '11 开始,未命名命名空间的成员隐式具有内部链接 (3.5/4):

未命名命名空间或在未命名命名空间中直接或间接声明的命名空间具有内部链接。

但与此同时,更新了 14.6.4.2/1 以删除对链接的提及(这取自 C++ '14):

对于后缀表达式是从属名称的函数调用,候选函数可以使用 通常的查找规则(3.4.1、3.4.2)除了:

  • 对于使用非限定名称查找 (3.4.1) 的部分查找,只能找到来自模板定义上下文的函数声明。

  • 对于使用关联命名空间 (3.4.2) 的查找部分,只能找到在模板定义上下文或模板实例化上下文中找到的函数声明。

结果是静态和未命名命名空间成员之间的这种特殊差异不再存在。

【讨论】:

  • export关键字不应该是冷死的吗?唯一支持“导出”的编译器是实验性编译器,除非意外,否则“导出”甚至不会在其他编译器中实现,因为意外的副作用(除了没有按照预期进行)
  • 查看 Herb Sutter 关于 subjet 的文章:gotw.ca/publications/mill23-x.htm
  • 爱迪生设计集团 (EDG) 的前端绝不是实验性的。它几乎可以肯定是世界上最符合标准的 C++ 实现。英特尔 C++ 编译器使用 EDG。
  • 什么 C++ 特性没有“意外的副作用”?在导出的情况下,将从不同的 TU 中找到未命名的命名空间函数 - 这与您直接包含模板定义相同。如果不是这样,那就更令人惊讶了!
  • 我认为你有一个错字 - 为了让 NS::S 工作,S 不需要在 namespace {} 内吗?
【解决方案4】:

我最近开始在我的代码中用匿名命名空间替换静态关键字,但立即遇到了一个问题,即命名空间中的变量不再可用于在我的调试器中进行检查。我使用的是 VC60,所以我不知道这是否与其他调试器无关。我的解决方法是定义一个“模块”命名空间,并在其中给它指定了我的 cpp 文件的名称。

例如,在我的 XmlUtil.cpp 文件中,我为我的所有模块变量和函数定义了一个命名空间 XmlUtil_I { ... }。这样我就可以在调试器中应用XmlUtil_I:: 限定来访问变量。在这种情况下,_I 将它与我可能想在其他地方使用的公共命名空间(例如 XmlUtil)区分开来。

我认为与真正匿名的方法相比,这种方法的一个潜在缺点是有人可能会通过在其他模块中使用命名空间限定符来违反所需的静态范围。不过,我不知道这是否是一个主要问题。

【讨论】:

  • 我也这样做了,但是使用#if DEBUG namespace BlahBlah_private { #else namespace { #endif,所以“模块命名空间”只存在于调试版本中,否则使用真正的匿名命名空间。如果调试器提供了一种很好的方法来处理这个问题,那就太好了。 Doxygen 也被它弄糊涂了。
  • 未命名的命名空间并不是真正可行的静态替代品。 static 的意思是“真的这永远不会在 TU 之外被链接”。未命名的命名空间意味着“它仍然作为一个随机名称导出,以防它被 TU 之外的父类调用”...
【解决方案5】:

根据经验,我只是注意到,虽然这是将以前的静态函数放入匿名命名空间的 C++ 方式,但旧的编译器有时会遇到问题。我目前为我们的目标平台使用了一些编译器,更现代的 Linux 编译器可以将函数放入匿名命名空间。

但是,在 Solaris 上运行的旧编译器,在未指定的未来版本之前,我们一直使用它,有时会接受它,而有时会将其标记为错误。错误不是我担心的问题,而是当它接受它时它可能正在做的事情。因此,在我们全面实现现代化之前,我们仍然使用静态(通常是类范围)函数,我们更喜欢匿名命名空间。

【讨论】:

    【解决方案6】:

    我个人更喜欢静态函数而不是无名命名空间,原因如下:

    • 仅从函数定义就可以清楚地看出,它对于编译它的翻译单元是私有的。对于无名命名空间,您可能需要滚动并搜索以查看函数是否在命名空间中。

    • 命名空间中的函数可能会被某些(较旧的)编译器视为外部函数。在 VS2017 中,它们仍然是外部的。因此,即使函数位于无名命名空间中,您可能仍希望将它们标记为静态。

    • 静态函数在 C 或 C++ 中的行为非常相似,而无名命名空间显然仅适用于 C++。无名命名空间也会在缩进中增加额外的层次,我不喜欢这样:)

    所以,我很高兴看到函数 isn't deprecated anymore 使用静态。

    【讨论】:

    • 匿名命名空间中的函数应该有外部链接。它们只是被破坏以使它们独一无二。只有static 关键字实际上将本地链接应用于函数。此外,肯定只有疯狂的疯子才会真正为命名空间添加缩进?
    • @Roflcopter4 许多编辑器默认为命名空间添加缩进。
    【解决方案7】:

    C++98 标准不推荐为此目的使用 static 关键字。 static 的问题在于它不适用于类型定义。它也是一个在不同上下文中以不同方式使用的重载关键字,因此未命名的命名空间稍微简化了一些事情。

    【讨论】:

    • 如果您只想在单个翻译单元中使用类型,请在 .cpp 文件中声明它。无论如何,其他翻译单元都无法访问它。
    • 你会想,不是吗?但是,如果同一应用程序中的另一个翻译单元(=cpp-file)曾经声明了一个具有相同名称的类型,那么您将面临相当难以调试的问题:-)。例如,您可能会遇到在调用另一种类型的方法时使用其中一种类型的 vtable 的情况。
    • 不再被弃用。并且 type defs 没有被导出,所以这是没有意义的。静态对于独立函数和全局变量很有用。未命名的命名空间对类很有用。
    【解决方案8】:

    不同之处在于被破坏的标识符的名称(_ZN12_GLOBAL__N_11bE vs _ZL1b,这并不重要,但它们都被组装成符号表中的局部符号(没有 .global asm 指令) .

    #include<iostream>
    namespace {
       int a = 3;
    }
    
    static int b = 4;
    int c = 5;
    
    int main (){
        std::cout << a << b << c;
    }
    
            .data
            .align 4
            .type   _ZN12_GLOBAL__N_11aE, @object
            .size   _ZN12_GLOBAL__N_11aE, 4
    _ZN12_GLOBAL__N_11aE:
            .long   3
            .align 4
            .type   _ZL1b, @object
            .size   _ZL1b, 4
    _ZL1b:
            .long   4
            .globl  c
            .align 4
            .type   c, @object
            .size   c, 4
    c:
            .long   5
            .text
    

    对于嵌套的匿名命名空间:

    namespace {
       namespace {
           int a = 3;
        }
    }
    
            .data
            .align 4
            .type   _ZN12_GLOBAL__N_112_GLOBAL__N_11aE, @object
            .size   _ZN12_GLOBAL__N_112_GLOBAL__N_11aE, 4
    _ZN12_GLOBAL__N_112_GLOBAL__N_11aE:
            .long   3
    

    翻译单元中所有第一级匿名命名空间相互结合,翻译单元中所有第二级嵌套匿名命名空间相互结合

    您还可以在匿名命名空间中拥有嵌套命名空间或嵌套内联命名空间

    namespace {
       namespace A {
           int a = 3;
        }
    }
    
            .data
            .align 4
            .type   _ZN12_GLOBAL__N_11A1aE, @object
            .size   _ZN12_GLOBAL__N_11A1aE, 4
    _ZN12_GLOBAL__N_11A1aE:
            .long   3
    
    which for the record demangles as:
            .data
            .align 4
            .type   (anonymous namespace)::A::a, @object
            .size   (anonymous namespace)::A::a, 4
    (anonymous namespace)::A::a:
            .long   3
    
    //inline has the same output
    

    您也可以使用匿名内联命名空间,但据我所知,inline 在匿名命名空间上的效果为 0

    inline namespace {
       inline namespace {
           int a = 3;
        }
    }
    

    _ZL1b: _Z 表示这是一个错误的标识符。 L 表示它是通过static 的本地符号。 1 是标识符b 的长度,然后是标识符b 的长度

    _ZN12_GLOBAL__N_11aE _Z 表示这是一个损坏的标识符。 N 表示这是一个命名空间12 是匿名命名空间名称_GLOBAL__N_1 的长度,然后是匿名命名空间名称_GLOBAL__N_1,那么1 是标识符a 的长度,a 是标识符 aE 关闭了位于命名空间中的标识符。

    _ZN12_GLOBAL__N_11A1aE 和上面一样,只是里面多了一个命名空间(1A),叫做A,前缀为A,长度为1。匿名命名空间的名字都是_GLOBAL__N_1

    【讨论】:

      【解决方案9】:

      另外,如果像这个例子一样在变量上使用 static 关键字:

      namespace {
         static int flag;
      }
      

      它不会在映射文件中看到

      【讨论】:

      • 那你根本不需要匿名命名空间。
      【解决方案10】:

      匿名命名空间和静态函数之间的编译器特定差异可以在编译以下代码时看到。

      #include <iostream>
      
      namespace
      {
          void unreferenced()
          {
              std::cout << "Unreferenced";
          }
      
          void referenced()
          {
              std::cout << "Referenced";
          }
      }
      
      static void static_unreferenced()
      {
          std::cout << "Unreferenced";
      }
      
      static void static_referenced()
      {
          std::cout << "Referenced";
      }
      
      int main()
      {
          referenced();
          static_referenced();
          return 0;
      }
      

      使用 VS 2017(指定级别 4 警告标志 /W4 以启用 warning C4505: unreferenced local function has been removed)和带有 -Wunused-function 或 -Wall 标志的 gcc 4.9 编译此代码表明 VS 2017 只会对未使用的静态产生警告功能。 gcc 4.9 及更高版本,以及 clang 3.3 及更高版本,将对命名空间中未引用的函数产生警告,并对未使用的静态函数产生警告。

      Live demo of gcc 4.9 and MSVC 2017

      【讨论】:

        【解决方案11】:

        刚刚在阅读您的问题时才了解此功能,我只能推测。这似乎比文件级静态变量提供了几个优点:

        • 匿名命名空间可以相互嵌套,提供多级保护,符号无法逃脱。
        • 多个匿名命名空间可以放在同一个源文件中,实际上在同一个文件中创建不同的静态级别范围。

        我有兴趣了解是否有人在实际代码中使用过匿名命名空间。

        【讨论】:

        • 不错的推测,但错误。这些命名空间的范围是文件范围的。
        • 不完全正确,如果您在另一个命名空间内定义匿名命名空间,它仍然只是文件范围,并且只能被视为在该命名空间内。试试看。
        • 我可能是错的,但我想不,它不是文件范围的:它只能被匿名命名空间之后的代码访问。这是一个微妙的事情,通常,我不想用多个匿名命名空间污染源......不过,这可以有用途。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-11-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-01-06
        • 1970-01-01
        相关资源
        最近更新 更多