天天看點

Google C++每周貼士 #153: 别用using訓示每周貼士 #153: 别用using訓示

(原文連結: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

    和命名空間

    ::testing

    聲明了同名的符号,那對該名稱的未限定查找将會編譯失敗。這很重要,因為它示範了一個可怕的規模問題:因為每個命名空間的全部内容都(一般而言)被注入了全局命名空間,每一個新的using訓示都可能增加平方級的命名沖突和建構失敗的風險。
  • 如果一個形如

    ::rpc::testing

    的子命名空間被引入,代碼就會編譯失敗。(實際上我們(譯者注:Google)已經見過那個命名空間了,是以這段代碼和那個命名空間相遇隻是時間問題。這也是避免深層的嵌套命名空間的另一個原因。)命名空間限定的缺失在這裡很重要:如果using訓示被完全限定 而且 沒有對兩個命名空間内共有的名字的非限定的命名查找,那麼這個代碼片段還是有可能編譯通過的。
  • ::totw::example

    ::totw

    ::testing

    ::rpc

    或全局命名空間裡引入一個新的符号,有可能跟 任一命名空間 中的已有符号沖突。這是個天坑。

悄悄話:你覺得

RPC

在哪個命名空間内?

rpc

是個合理的猜測,但它實際上在全局命名空間裡。除了維護問題以外,這裡的using訓示還讓代碼很難讀。

那這個特性為什麼會存在?

在通用的庫中有using訓示的合理應用,但它們既晦澀又少見,以至于不值得在這裡或風格指南中花篇幅讨論它們。

臨别贈言

using訓示是定時炸彈:今天還能編譯的代碼,在下一個語言版本或增加符号之後,分分鐘編譯失敗給你看。對于短命且依賴關系從不改變的外層代碼,這也許是一個可接受的風險。但是請注意:如果你後來決定想要讓你的臨時項目在長時間内繼續工作,這些定時炸彈也許會爆炸。

繼續閱讀