大家好,這個專欄會分析 RapidJSON (中文使用手冊)中一些有趣的 C++ 程式碼,希望對讀者有所裨益。
C++ 語法解說
我們先來看一行程式碼(document.h):
bool StartArray() {
new (stack_.template Push<ValueType>()) ValueType(kArrayType); // <--
return true;
}
或許你會問,這是什麼C++語法?
這裡其實用了兩個可能較少接觸的C++特性。第一個是 placement new,第二個是 template disambiguator。
Placement new
簡單來說,placement new 就是不分配記憶體,由使用者給予記憶體空間來構建物件。其形式是:
new (T*) T(...);
第一個括號中的是給定的指標,它指向足夠放下 T 型別的記憶體空間。而 T(...) 則是一個建構函式呼叫。那麼,上面 StartArary() 裡的程式碼,分開來寫就是:
bool StartArray() {
ValueType* v = stack_.template Push<ValueType>(); // (1)
new (v) ValueType(kArrayType); // (2)
return true;
}
這麼分拆,(2)應該很容易理解吧。那麼(1)是什麼樣的語法?為什麼中間會有 template 這個關鍵字?
template disambiguator
(1)其實只是呼叫 Stack 類的模板成員函式 Push()。如果刪去這個 template,程式碼就顯得正常一點:
ValueType* v = stack_.Push<ValueType>(); // (1)
這裡 Push
理解這些語法之後,我們進入核心問題。
混合任意型別的堆疊
處理樹狀的資料結構時,我們經常需要用到堆疊(stack)這種資料結構。C++ 標準庫也提供了 std::stack 這個容器。然而,這個模板類容器的例項,只能存放一種型別的物件。在 RapidJSON 的解析過程中,我們希望它能同時存放已解析的 Value 物件,以及 Member 物件(key-value對)。或者我們從另一個角度去想,程式堆疊(program stack)本身就是可儲存各種型別資料的堆疊。在 RapidJSON 中的其它地方也有這種需求。
在 internal/stack.h 中的 Stack 類實現了這個構思,其宣告是這樣的:
class Stack {
Stack(Allocator* allocator, size_t stackCapacity);
~Stack();
void Clear();
void ShrinkToFit();
template<typename T> T* Push(size_t count = 1);
template<typename T> T* Pop(size_t count);
template<typename T> T* Top();
template<typename T> T* Bottom();
Allocator& GetAllocator();
bool Empty() const;
size_t GetSize();
size_t GetCapacity();
};
這個類比較特殊的地方,就是堆疊操作使用模板成員函式,可以壓入或彈出不同型別的物件。另外,為了完全防止拷貝建構函式呼叫的可能性,這些函式都是返回指標。雖然引用也可以,但使用指標在一些應用情況下會更自然。
例如,要壓入4個 int,再每次彈出兩個:
Stack s;
*s.Push<int>() = 1;
*s.Push<int>() = 2;
*s.Push<int>() = 3;
*s.Push<int>() = 4;
for (int i = 0; i < 2; i++) {
int* a = s.Pop<int>(2);
std::cout << a[0] << " " << a[1] << std::endl;
}
// 輸出:
// 3 4
// 1 2
注意到,Pop() 返回彈出的最底端元素的指標,我們仍然可以通過這指標合法地訪問這些彈出的元素。
重要事項(坑出沒注意)
在 StartArray() 的例子裡,我們看到使用 placement new 來構建物件。在普通的情況下,new 和 delete 應該是成雙成對的,但使用了 placement new,就通常不能使用 delete,因為 delete 會呼叫解構函式並釋放記憶體。在這個例子裡,stack_ 物件提供了記憶體空間,所以我們只需要呼叫 ValueType 的解構函式。例如,如果解析在中途終止了,我們要手動彈出已入棧的 ValueType 並呼叫其解構函式:
while (!stack_.Empty())
(stack_.template Pop<ValueType>(1))->~ValueType();
另一個問題是,如果壓入不同的資料型別,可能會有記憶體對齊問題,例如:
Stack s;
*s.Push<char>() = 'f';
*s.Push<char>() = 'o';
*s.Push<char>() = 'o';
*s.Push<int >() = 123; // 對齊問題
123寫入的地址不是4的倍數,在一些CPU下可能造成崩潰。如果真的要做緊湊的packing,可以用 std::memcpy:
int i = 123;
std::memcpy(s.Push<int>(), &i, sizeof(i));
int j;
std::memcpy(&j, s.Pop<int>(1), sizeof(j));
程式碼複用
由於 RapidJSON 不依賴於 STL,在實現一些功能時缺少一些容器的幫忙。後來想到,一些地方其實可以把 Stack 當作可動態縮放的緩衝區來使用。例如,我們想從DOM生成JSON的字串,就實現了 GenericStringBuffer:
template <typename Encoding, typename Allocator = CrtAllocator>
class GenericStringBuffer {
public:
typedef typename Encoding::Ch Ch;
// ...
void Put(Ch c) { *stack_.template Push<Ch>() = c; }
const Ch* GetString() const {
// Push and pop a null terminator. This is safe.
*stack_.template Push<Ch>() = '\0';
stack_.template Pop<Ch>(1);
return stack_.template Bottom<Ch>();
}
size_t GetSize() const { return stack_.GetSize(); }
// ...
mutable internal::Stack<Allocator> stack_;
};
想在緩衝器末端加入字元,就使用 Stack::Push
結語
RapidJSON 為了一些記憶體及效能上的優化,萌生了一個混合任意型別的堆疊類 rapidjson::internal::Stack。但使用這個類要比 STL 提供的容器危險,必須清楚每個操作的具體情況、記憶體對齊等問題。而帶來的好處是更自由的容器內容型別,可以達到高快取一致性(用多個 std::stack 不利此因素),並且避免不必要記憶體分配、釋放、物件拷貝構造等。從另一個角度看,這個類更像一種特殊的記憶體分配器。