C++ 工程实践(1):慎用匿名 namespace

匿名 namespace (anonymous namespace 或称 unnamed namespace) 是 C++ 的一项非常有用的功能,其主要目的是让该 namespace 中的成员(变量或函数)具有独一无二的全局名称,避免名字碰撞 (name collisions)。一般在编写 .cpp 文件时,如果需要写一些小的 helper 函数,我们常常会放到匿名 namespace 里。muduo 0.1.7 中的 muduo/base/Date.cc 和 muduo/base/Thread.cc 等处就用到了匿名 namespace。

我最近在工作中遇到并重新思考了这一问题,发现匿名 namespace 并不是多多益善。

C 语言的 static 关键字的两种用法

C 语言的 static 关键字有两种用途:

1. 用于函数内部修饰变量,即函数内的静态变量。这种变量的生存期长于该函数,使得函数具有一定的“状态”。使用静态变量的函数一般是不可重入的,也不是线程安全的。

2. 用在文件级别(函数体之外),修饰变量或函数,表示该变量或函数只在本文件可见,其他文件看不到也访问不到该变量或函数。专业的说法叫“具有 internal linkage”(简言之:不暴露给别的 translation unit)。

C 语言的这两种用法很明确,一般也不容易混淆。

C++ 语言的 static 关键字的四种用法

由于 C++ 引入了 class,在保持与 C 语言兼容的同时,static 关键字又有了两种新用法:

3. 用于修饰 class 的数据成员,即所谓“静态成员”。这种数据成员的生存期大于 class 的对象(实体 instance)。静态数据成员是每个 class 有一份,普通数据成员是每个 instance 有一份,因此也分别叫做 class variable 和 instance variable。

4. 用于修饰 class 的成员函数,即所谓“静态成员函数”。这种成员函数只能访问 class variable 和其他静态程序函数,不能访问 instance variable 或 instance method。

当然,这几种用法可以相互组合,比如 C++ 的成员函数(无论 static 还是 instance)都可以有其局部的静态变量(上面的用法 1)。对于 class template 和 function template,其中的 static 对象的真正个数跟 template instantiation (模板具现化)有关,相信学过 C++ 模板的人不会陌生。

可见在 C++ 里 static 被 overload 了多次。匿名 namespace 的引入是为了减轻 static 的负担,它替换了 static 的第 2 种用途。也就是说,在 C++ 里不必使用文件级的 static 关键字,我们可以用匿名 namespace 达到相同的效果。(其实严格地说,linkage 或许稍有不同,这里不展开讨论了。)

匿名 namespace 的不利之处

在工程实践中,匿名 namespace 有两大不利之处:

  1. 其中的函数难以设断点,如果你像我一样使用的是 gdb 这样的文本模式 debugger。
  2. 使用某些版本的 g++ 时,同一个文件每次编译出来的二进制文件会变化,这让某些 build tool 失灵。

考虑下面这段简短的代码 (anon.cc):

   1:

namespace
   2:

{
   3:

void
foo()
   4:

{
   5:

}
   6:

}
   7:

 
   8:

int
main()
   9:

{
  10:

foo();
  11:

}

对于问题 1:

gdb 的键自动补全功能能帮我们设定断点,不是什么大问题。前提是你知道那个"(anonymous namespace)::foo() "正是你想要的函数。

$ gdb ./a.out
GNU gdb (GDB) 7.0.1-debian

(gdb) b '
(anonymous namespace)         __data_start                  _end
(anonymous namespace)::foo()   __do_global_ctors_aux         _fini
_DYNAMIC                      __do_global_dtors_aux         _init
_GLOBAL_OFFSET_TABLE_         __dso_handle                  _start
_IO_stdin_used                __gxx_personality_v0          anon.cc
__CTOR_END__                  __gxx_personality_v0@plt      call_gmon_start
__CTOR_LIST__                 __init_array_end              completed.6341
__DTOR_END__                  __init_array_start            data_start
__DTOR_LIST__                 __libc_csu_fini               dtor_idx.6343
__FRAME_END__                 __libc_csu_init               foo
__JCR_END__                   __libc_start_main             frame_dummy
__JCR_LIST__                  __libc_start_main@plt         int
__bss_start                   _edata                        main

(gdb) b '(
anonymous namespace)         anonymous namespace)::foo()

(gdb) b '(anonymous namespace)::foo()'
Breakpoint 1 at 0x400588: file anon.cc, line 4.

麻烦的是,如果两个文件 anon.cc 和 anonlib.cc 都定义了匿名空间中的 foo() 函数(这不会冲突),那么 gdb 无法区分这两个函数,你只能给其中一个设断点。或者你使用 文件名:行号 的方式来分别设断点。(从技术上,匿名 namespace 中的函数是 local text,链接的时候如果发生符号重名,linker 不会报错。)

从根本上解决的办法是使用普通具名 namespace,如果怕重名,可以把源文件名(必要时加上路径)作为 namespace 名字的一部分。

 

同样的道理,在 profiler 的输出中也不容易辨识 (anonymous namespace)::foo() 到底是哪个文件中的哪个函数,如果这样同名的函数多于一个,也会造成困扰。

对于问题 2:

把它编译两次,分别生成 a.out 和 b.out:

$ g++ -g -o a.out anon.cc

$ g++ -g -o b.out anon.cc

$ md5sum a.out b.out
0f7a9cc15af7ab1e57af17ba16afcd70  a.out
8f22fc2bbfc27beb922aefa97d174e3b  b.out

$ g++ --version
g++ (GCC) 4.2.4 (Ubuntu 4.2.4-1ubuntu4)

$ diff -u <(nm a.out) <(nm b.out)
--- /dev/fd/63  2011-02-15 22:27:58.960754999 +0800
+++ /dev/fd/62  2011-02-15 22:27:58.960754999 +0800
@@ -2,7 +2,7 @@
0000000000600940 d _GLOBAL_OFFSET_TABLE_
0000000000400634 R _IO_stdin_used
                  w _Jv_RegisterClasses
-0000000000400538 t _ZN36_GLOBAL__N_anon.cc_00000000_E2CEEB513fooEv
+0000000000400538 t _ZN36_GLOBAL__N_anon.cc_00000000_CB51498D3fooEv
0000000000600748 d __CTOR_END__
0000000000600740 d __CTOR_LIST__
0000000000600758 d __DTOR_END__

由上可见,g++ 4.2.4 会随机地给匿名 namespace 生成一个惟一的名字(foo() 函数的 mangled name 中的 E2CEEB51 和 CB51498D 是随机的),以保证名字不冲突。也就是说,同样的源文件,两次编译得到的二进制文件内容不相同,这有时候会造成问题。比如说拿到一个会发生 core dump 的二进制可执行文件,无法确定它是由哪个 revision 的代码编译出来的。毕竟编译结果不可复现,具有一定的随机性。

这可以用 gcc 的 -frandom-seed 参数解决,具体见文档。

这个现象在 gcc 4.2.4 中存在(之前的版本估计类似),在 gcc 4.4.5 中不存在。

替代办法

如果前面的“不利之处”给你带来困扰,解决办法也很简单,就是使用普通具名 namespace。当然,要起一个好的名字,比如 boost 里就常常用 boost::detail 来放那些“不应该暴露给客户,但又不得不放到头文件里”的函数或 class。

 

总而言之,匿名 namespace 没什么大问题,使用它也不是什么过错。万一它碍事了,可以用普通具名 namespace 替代之。

你可能感兴趣的:(C++,gcc,Class,语言,translation,Instantiation)