[022] 看 Code 說故事:Facebook Folly(二)- Lazy

[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

「第一次使用時才執行」可用來避免付出不必要的成本,若「成本」很貴時,好處更明顯。舉例,假設程式有一執行路徑需要網路連線,而進行網路連線前必須先做「初始化」,作法有兩種:

  1. 程式一執行就「初始化」網路相關設定,以便之後連線時能順利進行。
  2. 程式執行初期先不做「初始化」,直到需要網路前才去做初始化。

由於「連線至網路」只有在特定時機才會發生,很有可能一直到程式結束都不會遇到,沒有道理付出這不必要的開銷。第二種作法可用 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_ 改用結尾底線。