(原文連結:https://abseil.io/tips/153 譯者:[email protected])
每周貼士 #153: 别用using訓示
- 最初釋出于:2018-07-17
- 作者:Roman Perepelitsa 和 Ashley Hedberg
- 更新于:2020-04-06
- 短連結:abseil.io/tips/153
在我看來using訓示就是定時炸彈,不論是對它處理的部分還是類型系統。 – Ashley Hedberg 帶着對Warren Buffett的歉意
長話短說(tl;dr)
using訓示(
using namespace foo
)足夠危險,以至于被Google風格指南禁用了。
如果你想讓名字短一點,可以改用命名空間别名(
namespace baz = ::foo::bar::baz;
)或using聲明(
using ::foo::SomeName
),它倆都在一定的上下文中被風格指南所允許(例如,在
*.cc
檔案中)。
函數作用域中的using訓示
你覺得這段代碼幹了什麼?
namespace totw {
namespace example {
namespace {
TEST(MyTest, UsesUsingDirectives) {
using namespace ::testing;
Sequence seq; // ::testing::Sequence
WallTimer timer; // ::WallTimer
...
}
} // namespace
} // namespace example
} // namespace totw
絕大多數C++使用者都認為,這裡的using訓示把名字注入到了它被聲明的地方。在上面的例子中,也就是函數作用域。事實上,這些名字被注入到了目标命名空間(
::testing
)和用例命名空間(
::totw::example::anonymous
)的最近的公共祖先裡。在我們的例子中,這貨是全局命名空間!
是以,這段代碼大概相當于:
using ::testing::Expectation;
using ::testing::Sequence;
using ::testing::UnorderedElementsAre;
...
// 很多,很多符号被注入到了全局命名空間
namespace totw {
namespace example {
namespace {
TEST(MyTest, UsesUsingDirectives) {
Sequence seq; // ::testing::Sequence
WallTimer timer; // ::WallTimer
...
}
} // namespace
} // namespace example
} // namespace totw
這個轉換不是很精确,畢竟這些名字沒有真的在using訓示所在的作用域之外 保持 可見。然而,即使臨時地注入到全局作用域也會導緻不太妙的後果。
讓我們來看看什麼樣的修改可以把代碼整壞:
- 如果任何人定義了
或::totw::Sequence
,::totw::example::Sequence
将會指代那個實體而不是seq
。::testing::Sequence
- 如果任何人定義了
,::Sequence
的定義将會編譯失敗,因為對seq
這個名字的引用将會産生歧義。Sequence
可能意味着Sequence
或::testing::Sequence
,編譯器不知道你想要哪個。::Sequence
- 如果任何人定義了
,::testing::WallTimer
的定義将會編譯失敗。timer
是以,單單一個函數裡的using訓示就已經為
::testing
、
::totw
、
::totw::example
和全局命名空間裡的符号增加了命名限制。允許using訓示,就算隻是在函數作用域中,也在全局和其他命名空間中創造了足夠多的命名沖突的機會。
如果那個例子看起來還不夠脆,考慮下這個:
namespace totw {
namespace example {
namespace {
TEST(MyTest, UsesUsingDirectives) {
using namespace ::testing;
EXPECT_THAT(..., proto::Partially(...)); // ::testing::proto::Partially
...
}
} // namespace
} // namespace example
} // namespace totw
這個using訓示在全局作用域中引入了一個命名空間别名
proto
,大概相當于:
namespace proto = ::testing::proto;
namespace totw {
namespace example {
namespace {
TEST(MyTest, UsesUsingDirectives) {
EXPECT_THAT(..., proto::Partially(...)); // ::testing::proto::Partially
...
}
} // namespace
} // namespace example
} // namespace totw
該測試會一直通過編譯,直到間接地include了一個定義了命名空間
::proto
、
::totw::proto
或
::totw::example::proto
的頭檔案為止。到那個時候,
proto::Partially
變得有歧義了,該測試編譯失敗。這就聯系到了風格指南裡的命名空間命名規則:避免嵌套命名空間,而且不要用常用名字為嵌套命名空間起名字。(更多相關資訊,請參考Tip #130和命名空間名字)
有同學可能覺得,對一個封閉的、隻有很少符号且保證不會增加符号的命名空間,使用using訓示就是安全的。(隻有符号
_1
……
_9
的
std::placeholders
就是這種命名空間的一個例子。)然而,就算這樣也不安全:它阻止了其他任何命名空間引入同名的符号。在這個意義上,using訓示打破了命名空間提供的子產品性。
未限定的using訓示
我們已經見識過單個using訓示可能走到溝裡。如果在同一個代碼庫裡有很多——未限定的——using訓示,那将是一份怎樣的精彩呢?
namespace totw {
namespace example {
namespace {
using namespace rpc;
using namespace testing;
TEST(MyTest, UsesUsingDirectives) {
Sequence seq; // ::testing::Sequence
WallTimer timer; // ::WallTimer
RPC rpc; // ...is this ::rpc::RPC or ::RPC?
...
}
} // namespace
} // namespace example
} // namespace totw
這還能錯到哪兒去?那可多了,因為實際上:
- 所有函數層面的例子裡的問題都仍然存在,而且加倍了:一次是
命名空間,一次是::testing
命名空間。::rpc
- 如果命名空間
和命名空間::rpc
聲明了同名的符号,那對該名稱的未限定查找将會編譯失敗。這很重要,因為它示範了一個可怕的規模問題:因為每個命名空間的全部内容都(一般而言)被注入了全局命名空間,每一個新的using訓示都可能增加平方級的命名沖突和建構失敗的風險。::testing
- 如果一個形如
的子命名空間被引入,代碼就會編譯失敗。(實際上我們(譯者注:Google)已經見過那個命名空間了,是以這段代碼和那個命名空間相遇隻是時間問題。這也是避免深層的嵌套命名空間的另一個原因。)命名空間限定的缺失在這裡很重要:如果using訓示被完全限定 而且 沒有對兩個命名空間内共有的名字的非限定的命名查找,那麼這個代碼片段還是有可能編譯通過的。::rpc::testing
- 在
、::totw::example
、::totw
、::testing
或全局命名空間裡引入一個新的符号,有可能跟 任一命名空間 中的已有符号沖突。這是個天坑。::rpc
悄悄話:你覺得
RPC
在哪個命名空間内?
rpc
是個合理的猜測,但它實際上在全局命名空間裡。除了維護問題以外,這裡的using訓示還讓代碼很難讀。
那這個特性為什麼會存在?
在通用的庫中有using訓示的合理應用,但它們既晦澀又少見,以至于不值得在這裡或風格指南中花篇幅讨論它們。
臨别贈言
using訓示是定時炸彈:今天還能編譯的代碼,在下一個語言版本或增加符号之後,分分鐘編譯失敗給你看。對于短命且依賴關系從不改變的外層代碼,這也許是一個可接受的風險。但是請注意:如果你後來決定想要讓你的臨時項目在長時間内繼續工作,這些定時炸彈也許會爆炸。