Skip to content

Commit

Permalink
update global variable translation
Browse files Browse the repository at this point in the history
welkineins committed Feb 5, 2016
1 parent e0280e9 commit 2c28ad0
Showing 1 changed file with 7 additions and 30 deletions.
37 changes: 7 additions & 30 deletions google-cpp-styleguide/scoping.rst
Original file line number Diff line number Diff line change
@@ -249,39 +249,16 @@ C++ 允許在函式內的任何位置宣告變數。我們鼓勵在盡可能小
2.4. 靜態和全域變數
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. tip::
禁止使用 ``class`` 類型的靜態或全域變數:它們會導致難以發現的 bug 和不確定的建構和解構子呼叫順序。不過 ``constexpr`` 變量除外,畢竟它們又不涉及動態初始化或解構。

靜態生存週期的對象,即包括了全域變數,靜態變量,靜態類成員變量和函式靜態變量,都必須是原生數據類型 (POD : Plain Old Data): 即 int, char 和 float, 以及 POD 類型的指標、陣列和結構體。

靜態變數的建構子、解構子和初始化的順序在 C++ 中是不確定的,甚至隨著構建變化而變化,導致難以發現的 bug. 所以除了禁用類類型的全域變量,我們也不允許用函式返回值來初始化 POD 變量,除非該函式不涉及(比如 getenv() 或 getpid())不涉及任何全域變量。(函式作用域裡的靜態變量除外,畢竟它的初始化順序是有明確定義的,而且只會在指令執行到它的宣告那裡才會發生。)

同理,全域和靜態變數在程式中斷時會被解構,無論所謂中斷是從 ``main()`` 返回還是對 ``exit()`` 的呼叫。析構順序正好與建構子呼叫的順序相反。但既然建構順序未定義,那麼析構順序當然也就不定了。比如,在程式結束時某靜態變量已經被析構了,但程式碼還在跑——比如其它線程——並試圖訪問它且失敗;再比如,一個靜態 string 變量也許會在一個引用了前者的其它變量析構之前被析構掉。

改善以上解構問題的辦法之一是用 ``quick_exit()`` 來代替 ``exit()`` 並中斷程式。它們的不同之處是前者不會執行任何析構,也不會執行 ``atexit()`` 所綁定的任何 handlers. 如果你想在執行 ``quick_exit()`` 來中斷時執行某 handler(比如刷新 log),你可以把它綁定到 ``_at_quick_exit()``. 如果你想在 ``exit()`` 和 ``quick_exit()`` 都用上該 handler, 都綁定上去。

綜上所述,我們只允許 POD 類型的靜態變數,即完全禁用 ``vector`` (使用 C 陣列替代) 和 ``string`` (使用 ``const char []``)。

如果你確實需要一個 ``class`` 類型的靜態或全域變數,可以考慮在 ``main()`` 函式或 ``pthread_once()`` 內初始化一個指標且永不回收。注意只能用 raw 指針,別用智慧指針,畢竟後者的解構子涉及到上文指出的不定順序問題。
禁止使用具有 `靜態生存週期 (static strage duration) <http://en.cppreference.com/w/cpp/language/storage_duration#Storage_duration>`__ 的類別函式:它們會因為不確定的建構和解構子呼叫順序而產生難以發現的臭蟲。不過 ``constexpr`` 變數除外,因為它們不牽涉到動態初始化或解構。

.. note:: Yang.Y 譯注:
全域變數、靜態變數、靜態類別成員變數和函式內靜態變數等都具有靜態生存週期,都必須是原始資料類型 (POD : Plain Old Data):即 int、chars 、floats 或前三者的指標、陣列和結構體。

上文提及的靜態變數泛指靜態生存週期的對象, 包括: 全域變量, 靜態變量, 靜態類成員變量, 以及函式靜態變量.
靜態變數的建構子、解構子和初始化的順序在 C++ 中規範並不完整,甚至可能每次建置都不同,進而導致難以發現的臭蟲。所以除了禁用類別的全域變量外,也不允許使用函式的返回值來初始化靜態變數,除非該函式(比如 getenv() 或 getpid())不涉及任何全域變數。但是函式作用域裡的 POD 變數則可以使用函式返回值來初始化,畢竟它的初始化順序是有明確定義的,而且只會在程式流程執行到它的宣告時才會發生。)

譯者 (YuleFox) 筆記
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
同理,全域和靜態變數在程式結束時會被解構,無論所謂結束是從 ``main()`` 返回還是呼叫了 ``exit()``。解構的順序被定義為其建構子呼叫順序的反序。但既然建構順序未定義,那麼解構順序當然也就不確定了。例如,在程式結束時某個靜態變數已經被解構了,但程式還在運行,這時可能有另一個執行緒嘗試要存取這個變數卻失敗了;另一個例子,一個靜態的 string 變量也許會在一個參考它的的其它變數被解構之前被解構掉。

#. ``cc`` 中的匿名命名空間可避免命名衝突, 限定作用域, 避免直接使用 ``using`` 關鍵字污染命名空間;
#. 巢狀類別符合局部使用原則, 只是不能在其他標頭檔中前置宣告, 盡量不要 ``public``;
#. 盡量不用全域函式和全域變數, 考慮作用域和命名空間限制, 盡量單獨形成編譯單元;
#. 多線程中的全域變數 (含靜態成員變量) 不要使用 ``class`` 類型 (含 STL 容器), 避免不明確行為導致的 bug.
#. 作用域的使用, 除了考慮名稱污染, 可讀性之外, 主要是為降低耦合, 提高編譯/執行效率.
解決以上解構順序問題的方法之一是用 ``quick_exit()`` 來代替 ``exit()`` 來結束程式。它們的不同之處是前者不會執行任何解構子,也不會執行 ``atexit()`` 所註冊的任何 handlers。如果在使用 ``quick_exit()`` 來結束成式時仍然想要執行 handler(例如 flush log),你可以使用 ``_at_quick_exit()`` 來註冊 handler。(如果你想在 ``exit()`` 和 ``quick_exit()`` 都用使用該 handler,則可以將其註冊到這兩個函式。

譯者(acgtyrant)筆記
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
綜上所述,我們只允許 POD 類型的靜態變數,這代表完全禁用 ``vector`` (使用 C 陣列替代) 和 ``string`` (使用 ``const char []``)。

#. 注意「using 指示(using-directive)」和「using 宣告(using-declaration)」的區別。
#. 匿名命名空間說白了就是文件作用域,就像 C static 宣告的作用域一樣,後者已經被 C++ 標準提倡棄用。
#. 區域變數在宣告的同時進行顯式值初始化,比起隱式初始化再賦值的兩步過程要高效,同時也貫徹了計算機體系結構重要的概念「局部性(locality)」。
#. 注意別在循環犯大量建構和解構的低級錯誤。
如果你真的需要一個 ``class`` 類型的靜態或全域變數,可以考慮在 ``main()`` 函式或 ``pthread_once()`` 內初始化一個指標且永不回收 (free)。注意只能用 raw 指標,別用智慧指標,因為後者的解構子涉及到上文指出的不定順序問題。

0 comments on commit 2c28ad0

Please sign in to comment.