2014年5月18日 星期日

C++ inline namespace

最近在研究 libc++ 的原始碼,意外的發現 inline namespace 這個有趣的功能。一言以蔽之,inline namespace 讓我們可以在 C++ 程式碼層級做 symbol versioning。

設想一個情境:你今天要設計一個函式庫。你可以預期這個函式庫的生命週期會很長,而且你未必能讓所有的使用者重新編譯他們的程式。這意謂著:你對 Object Layout 的任何改動都有可能破壞向下相容。這時候我們就會想要提供不同版本的函式。但是實務上該怎麼做呢?

舉例來說,我們要實作 std::string 類別。最早我們寫的版本是一個簡單的實作:

namespace std {
  class string {
  private:
    size_t size;
    size_t capacity;
    char *data;
  public:
    string(const char *s);
    const char *c_str() const {
      return data;
    }
  };

  string::string(const char *s)
      : size(std::strlen(s)), capacity(0), data(nullptr) {
    data = new char[size + 1];
    capacity = size + 1;
    memcpy(data, s, size + 1);
  }
}


可是隨著時間的演進,我們想採用 Small String Optimization (SSO) 以取得更高的效能與空間利用率。這時候我們必需把 std::string 改寫成:

namespace std {
  class string {
  private:
    size_t size;

    union {
      struct {
        size_t capacity;
        char *data;
      } normal;

      struct {
        char data[sizeof(size_t) + sizeof(char *)];
      } sso;
    };

  public:
    string(const char *s);

    const char *c_str() const {
      if (size < sizeof(sso.data)) {
        return sso.data;
      } else {
        return normal.data;
      }
    }
  };

  string::string(const char *s): size(std::strlen(s)) {
    if (size < sizeof(sso.data)) {
      memcpy(sso.data, s, size + 1);
    } else {
      normal.data = new char[size + 1];
      normal.capacity = size + 1;
      memcpy(normal.data, s, size + 1);
    }
  }
}


然而這個改動有可能會對舊的 Binary 造成困擾。如果 c_str() 有被 inline 但 string(const char *) 建構子沒有被 inline,則執行期會產生「字串內容」被當成 data_ 指標的嚴重問題。

我們該怎麼避免這個問題呢?我們可以用 Namespace 適當分割不同的實作:

namespace std {
  namespace v1 {
    class string { ... };
  }
  namespace v2 {
    class string { ... };
  }
}


可是這樣我們必須用 std::v1::string 或 std::v2::string 來指稱我們的 string 類別。這當然不滿足我們的期望。畢境所有的使用者還是使用 std::string 宣告一個字串。

這時就輪到 inline namespace 出場了!嚴格來說,inline namespace 是編譯器提供的 syntax sugar。他讓我們可以省略那個 namespace 的 scope 運算子。舉例來說,如果我要以 std::v2::string 的實作作為預設的版本,我可以把上面的例子改成:

namespace std {
  namespace v1 {
    class string { ... };
  }
  inline namespace v2 {
    class string { ... };
  }
}


這樣一來,我寫 std::string 其實就會是 std::v2::string。

當然,你也可以善用 Preprocessor 讓不同的程式可以依據特定條件選擇特定實作。例如:libfoo 是舊的函式庫,所以如果 libfoo 引入這個標頭檔,我們就把 v1 宣告為 inline namespace;libbar 是新的函式庫,所以如果 libbar 引入這個標頭檔,我們就把 v2 宣告為 inline namespace。

namespace std {
#if defined(LIBFOO)
  inline
#endif
  namespace v1 {
    class string { ... };
  }


#if !defined(LIBFOO)
  inline
#endif
  namespace v2 {
    class string { ... };
  }
}


附帶一提,inline namespace 只是 syntax sugar 對我們有一個實際上的好處:從 Linker 或 Loader 的角度來看,這些都是不同的函式,所以 std::v1::string 和 std::v2::string 的實作可以並存。不同來源的 Binary 可以各自取用他們所需的實作。

沒有留言:

張貼留言