你們好,這個專欄會分析 RapidJSON (中文使用手冊)中一些有趣的 C++ 代碼,但願對讀者有所裨益。git
咱們先來看一行代碼(document.h):github
bool StartArray() { new (stack_.template Push<ValueType>()) ValueType(kArrayType); // <-- return true; }
或許你會問,這是什麼C++語法?json
這裏其實用了兩個可能較少接觸的C++特性。第一個是 placement new,第二個是 template disambiguator。api
簡單來講,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 這個關鍵字?函數
(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 不利此因素),而且避免沒必要要內存分配、釋放、對象拷貝構造等。從另外一個角度看,這個類更像一種特殊的內存分配器。