C++ 工程實踐(1):慎用匿名 namespace

來源:互聯網
上載者:User

匿名 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 的<tab>鍵自動補全功能能幫我們設定斷點,不是什麼大問題。前提是你知道那個"(anonymous namespace)::foo()"正是你想要的函數。

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

(gdb) b '<tab>
(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 '(<tab>
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 中的函數是 weak text,連結的時候如果發生符號重名,linker 不會報錯。)

從根本上解決的辦法是使用普通具名 namespace,如果怕重名,可以把源檔案名稱(必要時加上路徑)作為 namespace 名字的一部分。

對於問題 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 替代之。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.