你们好,这个专栏会分析 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 不利此因素),而且避免没必要要内存分配、释放、对象拷贝构造等。从另外一个角度看,这个类更像一种特殊的内存分配器。