設想一個情境:你今天要設計一個函式庫。你可以預期這個函式庫的生命週期會很長,而且你未必能讓所有的使用者重新編譯他們的程式。這意謂著:你對 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 可以各自取用他們所需的實作。
沒有留言:
張貼留言