RapidJSON 代码剖析(一):混合任意类型的堆栈
大家好,这个专栏会分析 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 是一个 dependent name,它依赖于 ValueType 的实际类型。这里编译器不能确认 < 为小于运算符,还是模板的 <。为了避免歧义,需要加入template 关键字。这是C++标准的规定,缺少这个 template 关键字 gcc 和 clang 都会报错,而 vc 则会通过(C++标准也容许实现这样的编译器)。和这个语法相近的还有 typename disambiguator。
理解这些语法之后,我们进入核心问题。
混合任意类型的堆栈
处理树状的数据结构时,我们经常需要用到堆栈(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(),想把整个缓冲取出来,就简单地回传底端的指针。不过这里有个特别的地方,因为需要空字符作结尾,在 GetString() 时,会压入并立即弹出一个空字符。如前所述,弹出后、压入其他东西前,刚弹出的内容仍然是合法的。而由于我们希望GetString() 是 const 函数,所以这里让 stack_ 加上 mutable 修饰词。
结语
RapidJSON 为了一些内存及性能上的优化,萌生了一个混合任意类型的堆栈类 rapidjson::internal::Stack。但使用这个类要比 STL 提供的容器危险,必须清楚每个操作的具体情况、内存对齐等问题。而带来的好处是更自由的容器内容类型,可以达到高缓存一致性(用多个 std::stack 不利此因素),并且避免不必要内存分配、释放、对象拷贝构造等。从另一个角度看,这个类更像一种特殊的内存分配器。
RapidJSON 代码剖析(一):混合任意类型的堆栈的更多相关文章
- RapidJSON 代码剖析(四):优化 Grisu
		
我曾经在知乎的一个答案里谈及到 V8 引擎里实现了 Grisu 算法,我先引用该文的内容简单介绍 Grisu.然后,再谈及 RapidJSON 对它做了的几个底层优化. (配图中的<Grisù& ...
 - RapidJSON 代码剖析(三):Unicode 的编码与解码
		
根据 RFC-7159: 8.1 Character Encoding JSON text SHALL be encoded in UTF-8, UTF-16, or UTF-32. The defa ...
 - RapidJSON 代码剖析(二):使用 SSE4.2 优化字符串扫描
		
现在的 CPU 都提供了单指令流多数据流(single instruction multiple data, SIMD)指令集.最常见的是用于大量的浮点数计算,但其实也可以用在文字处理方面. 其中,S ...
 - (转载)Java Map中的Value值如何做到可以为任意类型的值
		
转载地址:http://www.importnew.com/15556.html 如有侵权,请联系作者及时删除. 搬到我的博客来,有空细细品味,把玩. 本文由 ImportNew - shut ...
 - Jquery UI 组合树 - ComboTree 集成Wabacus4.1 代码剖析
		
Jquery UI 1.3 (组合树 - ComboTree ) 集成Wabacus4.1 集成Spring 代码剖析 使用时,请下载需要Jquery ui包进行配置 combotree.js 的代码 ...
 - rapidjson代码封装类
		
rapidjson代码封装类 以下代码封装了rapidjson增删改查等基本操作: /********************************************************* ...
 - x264代码剖析(十三):核心算法之帧间预測函数x264_mb_analyse_inter_*()
		
x264代码剖析(十三):核心算法之帧间预測函数x264_mb_analyse_inter_*() 帧间预測是指利用视频时间域相关性,使用临近已编码图像像素预測当前图像的像素,以达到有效去除视频时域冗 ...
 - WPF:将Office文档、任意类型文件嵌入到EXE可执行文件中
		
原文:WPF:将Office文档.任意类型文件嵌入到EXE可执行文件中 版权声明:本文为博主原创文章,未经博主允许可以随意转载 https://blog.csdn.net/songqingwei198 ...
 - Golang  传递任意类型的切片
		
肯定有这样的一种场景,写一个函数,该函数可以接收任意类型的切片,完成相应的功能. 就好比这种情况 intSlice := []int{1,2,3,4,5,6,7,8} strSlice := []st ...
 
随机推荐
- 【webapp的优化整理】要做移动前端优化的朋友进来看看吧
			
单页or多页 本文仅代表个人观点,不足请见谅,欢迎赐教. webapp 小钗从事单页相关的开发一年有余,期间无比的推崇webapp的网站模式,也整理了很多移动开发的知识点,但是现在回过头来看,weba ...
 - “不要抄代码!自己的代码也不要抄!”
			
在 Adventure 位于深圳的电子设备组装厂(SZE)里,小朱狠狠的对自己说. 他刚刚在调试 STM32F407VG 的 SPI 功能.就在昨天,他刚刚调试好了 STM32F407VG 的 USA ...
 - iOS应用中的相关正则及验证
			
1.手机号码的验证正则 正则表达式: ^((13[0-9])|(15[^4,\\D])|(18[0,0-9]))\\d{8}$ 详细解释 解释: ^...$: ^:开始 $:结束 中间为要处理的字串 ...
 - SJPullDownMenu下拉菜单框架使用
			
SJPullDownMenu 快速集成类似淘宝筛选下拉菜单 如果页面显示不全等问题请转至:http://www.jianshu.com/p/d07c6393830c 查看使用 Getting Star ...
 - git笔记
			
这篇有关git的博客,写着写着有些崩了.里面有些碎碎念了.下次一定注意这个问题. 创建项目: midir xx :创建xx文件夹 git init : 为当前文件夹创建代码仓库 提交代码: git a ...
 - RMAN备份脚本一列分享
			
在ORACLE数据库中,RMAN备份的脚本非常多,下面介绍一例shell脚本如何通过RMAN备份,以及FTP上传RMAN备份文件以及归档日志文件的脚本. fullback.sh 里面调用RMAN命令做 ...
 - Error: 9001, Severity: 21, State: 5 The log for database 'xxxx' is not available
			
昨天下午5点多收到几封告警邮件,我还没有来得及看,GLE那边的同事就电话过来,说数据库出现告警了.让我赶紧看看,案例具体信息如下所示: 告警邮件内容: DATE/TIME: 2015/1/23 17: ...
 - SQL Server 2008 R2——统计各部门某年入职人数
			
=================================版权声明================================= 版权声明:原创文章 谢绝转载 请通过右侧公告中的“联系邮 ...
 - Forbidden You don't have permission to access / on this server PHP
			
在新安装的谷歌游览器里,打不了PHP网站了,错误显示: Forbidden You don't have permission to access / on this server. 原因还是配置权限 ...
 - WPF 提示框、确认框、确认输入框
			
1.提示框 分为提示.异常.失败.成功几种类型 方法: /// <summary> /// 弹出提示 /// 标题:提示 /// </summary> /// <para ...