为什么您更喜欢未命名的命名空间而不是静态函数?

Why should you prefer unnamed namespaces over static functions?

提问人:Head Geek 提问时间:10/1/2008 最后编辑:Jan SchultkeHead Geek 更新时间:9/26/2023 访问量:321177

问:

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

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

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

我的问题是,为什么或何时这比使用静态函数更可取?或者它们本质上是做完全相同事情的两种方式?

C++ 静态 命名空间 unnamed-namespace

评论

25赞 legends2k 12/6/2013
在 C++11 中,在此上下文中的用法未被弃用;尽管未命名命名空间是静态的更好替代品,但在某些情况下,静态来救援时,它会失败static

答:

421赞 luke 10/1/2008 #1

C++ 标准在第 7.3.1.1 节“未命名空间”第 2 段中写道:

static 关键字的用法是 在 namespace 作用域,即 unnamed-namespace 提供了一个更好的选择。

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

编辑:

弃用关键字(影响翻译单元中变量声明的可见性)的决定已被撤销(参考文献)。在这种情况下,使用 a 或 unnamed 基本上又回到了做完全相同事情的两种方式。有关更多讨论,请参阅 SO 问题。staticstaticnamespace

Unnamed 的优点是允许您定义 translation-unit-local 类型。有关更多详细信息,请参阅 SO 问题。namespace

感谢迈克·珀西(Mike Percy)引起了我的注意。

评论

45赞 mloskot 12/23/2009
Head Geek 询问仅针对函数使用的静态关键字。应用于在命名空间范围内声明的实体的 static 关键字指定其内部链接。在匿名命名空间中声明的实体具有外部链接 (C++/3.5),但可以保证它位于唯一命名的范围内。未命名命名空间的这种匿名性有效地隐藏了其声明,使其只能从翻译单元内部访问。后者的工作方式与静态关键字相同。
6赞 Alexander Oh 10/11/2011
外联的缺点是什么?这会影响内联吗?
24赞 Calmarius 8/15/2012
那些说静态关键字被弃用的 C++ 设计委员会的人可能从未在大型现实世界系统中使用过巨大的 C 代码......(如果匿名命名空间包含大量带有大注释块的声明,则您会立即看到一个静态关键字,但不会看到它。
3赞 Erik Aronesty 1/16/2015
未命名的命名空间会减慢链接速度。使用静态
5赞 underscore_d 1/14/2016
@ErikAronesty 这听起来不对。你有可重复的例子吗?从C++11开始 - 甚至在此之前,在某些编译器中 - 未命名的s隐式具有内部链接,因此应该没有区别。以前可能因措辞不当而产生的任何问题都通过将其作为 C++11 中的要求来解决。namespace
-1赞 Commodore Jaeger 10/1/2008 #2

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

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

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

评论

6赞 Konrad Rudolph 10/1/2008
好的猜测,但错了。这些命名空间的作用域是文件范围的。
1赞 Greg Rogers 10/1/2008
不完全正确,如果您在另一个命名空间中定义一个匿名命名空间,它仍然只是文件范围,并且只能被视为在该命名空间内。试试吧。
0赞 paercebal 10/12/2008
我可能是错的,但我想不,它不是文件范围的:它只能由匿名命名空间之后的代码访问。这是一件微妙的事情,通常,我不想污染具有多个匿名命名空间的源......尽管如此,这还是有用途的。
8赞 Firas Assaad 10/1/2008 #3

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

评论

1赞 Calmarius 8/15/2012
如果只想在单个翻译单元中使用类型,请在 .cpp 文件中声明它。无论如何,它都无法从其他翻译单位访问。
5赞 avl_sweden 11/11/2012
你会想,不是吗?但是,如果同一应用程序中的另一个翻译单元(=cpp-file)曾经声明过具有相同名称的类型,那么您将遇到相当难以调试的问题:-)。例如,在调用另一种类型的方法时,最终可能会使用其中一种类型的 vtable。
2赞 Erik Aronesty 1/16/2015
不再弃用。并且类型 defs 不会导出,所以这毫无意义。静态对于独立函数和全局变量很有用,未命名的命名空间对于类很有用。
94赞 hazzen 10/1/2008 #4

将方法放在匿名命名空间中可防止意外违反一个定义规则,从而使您永远不必担心将帮助程序方法命名为与可能链接的其他方法相同的方法。

而且,正如 Luke 所指出的,匿名命名空间在标准中比静态成员更受欢迎。

评论

6赞 Head Geek 10/1/2008
我指的是静态独立函数(即文件范围的函数),而不是静态成员函数。静态独立函数与未命名命名空间中的函数大致相同,因此存在问题。
2赞 hazzen 10/1/2008
啊;好吧,ODR仍然适用。编辑以删除段落。
0赞 Andriy Tylychko 7/27/2011
据我所知,静态函数的 ODR 在标头中定义并且此标头包含在多个翻译单元中时不起作用,对吧?在这种情况下,您会收到同一函数的多个副本
0赞 Nikita Vorontsov 3/12/2014
@Andy T:在包含标题的情况下,您不会真正看到“多个定义”。预处理器负责处理它。除非需要研究预处理器生成的输出,这在我看来相当奇特和罕见。此外,在头文件中包含“守卫”也是一种很好的做法,例如:“#ifndef SOME_GUARD - #define SOME_GUARD......”这应该可以防止预处理器两次包含相同的标头。
2赞 Alex 2/28/2019
@NikitaVorontsov保护可能会阻止将相同的标头包含在同一个翻译单元中,但它允许在不同的翻译单元中包含多个定义。这可能会导致“多定义”链接器错误。
12赞 William Knight 10/1/2008 #5

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

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

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

评论

8赞 Kristopher Johnson 8/6/2010
我也这样做了,但是使用 ,所以“模块命名空间”只存在于调试版本中,否则使用真正的匿名命名空间。如果调试器提供一种很好的方法来处理这个问题,那就太好了。Doxygen 也对此感到困惑。#if DEBUG namespace BlahBlah_private { #else namespace { #endif
6赞 Erik Aronesty 1/16/2015
未命名的命名空间并不是 static 的真正可行替代品。static 的意思是“实际上,这永远不会被链接到 TU 之外”。unnamed namespace 的意思是“它仍然作为随机名称导出,以防它从 TU 外部的父类调用”......
43赞 Richard Corden 10/1/2008 #6

有一种极端情况,静电会产生令人惊讶的效果(至少对我来说是这样)。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++ 编译器(即支持 的编译器),关键字仍将具有任何其他方式都无法使用的功能。exportstatic

// 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) 的查找部分,仅找到在模板定义上下文或模板实例化上下文中找到的函数声明。

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

评论

4赞 paercebal 10/12/2008
导出关键字不是应该冷死的吗?唯一支持“export”的编译器是实验性的,除非出现意外,否则“export”甚至不会在其他编译器中实现,因为意想不到的副作用(除了没有这样做之外)
2赞 paercebal 10/12/2008
参见 Herb Sutter 关于副喷气式飞机的文章:gotw.ca/publications/mill23-x.htm
3赞 Richard Corden 10/13/2008
爱迪生设计集团(EDG)的前端绝不是实验性的。几乎可以肯定,它是世界上最符合标准的C++实现。英特尔 C++ 编译器使用 EDG。
1赞 Richard Corden 10/13/2008
什么 C++ 功能没有“意想不到的副作用”?在导出的情况下,将从不同的 TU 中找到未命名的命名空间函数 - 这与直接包含模板定义相同。如果不是这样,那就更令人惊讶了!
0赞 Eric 10/10/2017
我想你那里有一个错别字 - 为了工作,不需要不在里面吗?NS::SSnamespace {}
8赞 Don Wakefield 10/27/2008 #7

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

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

2赞 Chris 12/9/2011 #8

此外,如果对变量使用 static 关键字,例如以下示例:

namespace {
   static int flag;
}

它不会在映射文件中看到

评论

9赞 Calmarius 8/15/2012
那么你根本不需要匿名命名空间。
3赞 masrtis 4/18/2017 #9

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

#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 以启用警告 C4505:未引用的本地函数已被删除)和带有 -Wunused-function 或 -Wall 标志的 gcc 4.9 编译此代码表明,VS 2017 只会对未使用的静态函数生成警告。GCC 4.9 及更高版本以及 CLANG 3.3 及更高版本将对命名空间中未引用的函数生成警告,也会对未使用的静态函数生成警告。

gcc 4.9 和 MSVC 2017 的现场演示

13赞 Pavel P 4/6/2018 #10

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

  • 仅从函数定义来看,很明显,它是编译它的翻译单元的私有的。使用无名称命名空间时,可能需要滚动并搜索以查看函数是否在命名空间中。

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

  • 静态函数在 C 或 C++ 中的行为非常相似,而无名命名空间显然只有 C++。无名命名空间还会在缩进中增加额外的级别,我不喜欢这样:)

因此,我很高兴看到不再弃对函数使用静态。

评论

1赞 Roflcopter4 9/12/2018
匿名命名空间中的函数应该具有外部链接。它们只是为了使它们独一无二而被破坏。只有关键字才能真正将本地链接应用于函数。另外,肯定只有狂热的疯子才会真正为命名空间添加缩进吗?static
0赞 Pavel P 11/17/2021
默认情况下,许多编辑器@Roflcopter4为命名空间添加缩进。
1赞 c z 5/27/2022
对我来说,最主要的是尝试调试。无名命名空间在编译后并不是无名的,它们只是得到一个不可键入的自动生成的名称。
10赞 Lewis Kelsey 6/3/2020 #11

区别在于损坏的标识符的名称 ( vs ,这并不重要,但它们都组装到符号表中的本地符号(没有 asm 指令)。_ZN12_GLOBAL__N_11bE_ZL1b.global

#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

您也可以拥有匿名内联命名空间,但据我所知,在匿名命名空间上具有 0 效果inline

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

_ZL1b:表示这是一个损坏的标识符。 表示它是通过 的本地符号。 是标识符的长度,然后是标识符_ZLstatic1bb

_ZN12_GLOBAL__N_11aE _Z表示这是一个被篡改的标识符。 表示 this is a namespace 是匿名命名空间名称的长度,然后是匿名命名空间名称 ,然后是标识符的长度,是标识符并关闭驻留在命名空间中的标识符。N12_GLOBAL__N_1_GLOBAL__N_11aaaE

_ZN12_GLOBAL__N_11A1aE与上面相同,只是其中有另一个命名空间 () 称为 ,前缀为 ,其长度为 1。匿名命名空间都具有1AAA_GLOBAL__N_1