[022] 看 Code 說故事:Facebook Folly(二)- Lazy
Facebook C++ library 檔案命名慣例為 FileName.h
,也就是 Camel Case 命名法,開頭大寫。我目前愛用的命名法是 Google 的 Snake case,長這樣:file_name.h
。
單元測試使用 Google C++ test framework,各大開源專案的一致選擇。測試碼統一放在 test
目錄(我覺得複數 tests
較為合適)。Chromium 專案的習慣是測試碼與被測碼檔案放在同一個目錄,我們的專案也採用這種慣例。
回到程式碼,先來看 Lazy
,路徑為:
- folly/Lazy.h
類似 Lazy evaluation 技法,其特性為:
- 讓某運算式/資源第一次使用時才執行/索取
- 運算式/資源只會被執行/索求一次
- Not thread-safe
「第一次使用時才執行」可用來避免付出不必要的成本,若「成本」很貴時,好處更明顯。舉例,假設程式有一執行路徑需要網路連線,而進行網路連線前必須先做「初始化」,作法有兩種:
- 程式一執行就「初始化」網路相關設定,以便之後連線時能順利進行。
- 程式執行初期先不做「初始化」,直到需要網路前才去做初始化。
由於「連線至網路」只有在特定時機才會發生,很有可能一直到程式結束都不會遇到,沒有道理付出這不必要的開銷。第二種作法可用 Lazy
來實作,搭配「只會被執行一次」的特性,不論做幾次網路連線,「初始化」的花費只要付一次。
我想,不將 Lazy
設計成 Thread-safe,一方面簡化設計,一方面有點 YAGNI 的味道。
來看看 folly\test\LazyTest.cpp
其中一個測試案例:
TEST(Lazy, Simple) {
int computeCount = 0;
auto const val = folly::lazy([&]() -> int {
++computeCount;
EXPECT_EQ(computeCount, 1);
return 12;
});
EXPECT_EQ(computeCount, 0);
for (int i = 0; i < 100; ++i) {
if (i > 50) {
EXPECT_EQ(val(), 12);
EXPECT_EQ(computeCount, 1);
} else {
EXPECT_EQ(computeCount, 0);
}
}
EXPECT_EQ(val(), 12);
EXPECT_EQ(computeCount, 1);
}
可以看到,val
不管被呼叫幾次,其包含的 lambda 只會被呼叫一次(computeCount
== 1)。
看實作程式碼有幾點發現:
- Lazy 大部分的程式碼被包在
namespace detail
。這麼做的好處是:- 告訴呼叫端
detail
裡的程式碼非 Lazy 的公開介面,使用者不必在乎該些實作細節。 - 對於 Header only 的函式庫來說,清楚分出公開介面與實作細節,避免混肴。
- 告訴呼叫端
- 大量使用 C++11 才有的新功能:
std::forward
std::remove_reference
std::result_of
- 成員變數(member variable)命名法使用結尾底線(
func_
),而不是另一派的m_func
。幾年前看了 Chromium 原始碼後,便由開頭m_
改用結尾底線。