右值引用、移动语义和完美转发

  C++作为一门极为讲求运行效率的编程语言,在新标准中引入了右值引用和移动语义,同时通过右值引用还可以实现参数的完美转发,方便进行通用模板编程。

一、右值引用

1.1 左值和右值

  在C++中,对象具有两种属性:(1)有身份:在程序有有对象的名字,或者指向该对象的指针,或者指向该对象的引用;(2)可移动:能否把对象的内容移动出来,比如把对象的值移动到其他对象中去,该对象移动后处于合法但是未指定的状态。
  所以,根据上面两种属性总共有四种组合出现,不过没有身份又无法移动的对象不重要也不需要讨论,因此可以看到:
right-val
  从上图看到,一个经典的左值有身份但是不能移动,一个经典的右值允许执行移出操作,而其中的特殊值常常是用std::move()方式得到的,他本身具有身份,但是通过上面的操作也允许其执行移出的许可。纯右值一般是函数以非引用方式返回的临时变量、运算表达式(比如运算、关系、位、后置递增递减等)产生的临时变量、原始字面量、lambda表达式等,都是纯右值的概念,这些对象即将被消亡且没有其他的用户;特殊值是C++11中新产生的,比如将要被移动的对象、T&&函数返回值、std::move()返回值和转换为T&&类型的转换函数的返回值。

1.2 右值引用

  在C++中,有左值引用、const左值引用、右值引用三种类型,他们的使用目的各不相同:普通左值引用所引用的对象表明用户可以执行写入、修改操作;const左值引用的对象对用户看来是不可修改的;右值引用对应于一个临时对象,用户可以修改这个对象,并且确认这个对象以后不会再用了。

C++之参数类型推导

  参数类型推演算是Modern C++中的重点内容,他们会在函数模板、auto、decltype中被用到,不过这东西确实很隐晦,想要搞透他们也的确不容易,经常被弄得头晕晕的。但是这种硬骨头不啃也不得行,绕过他想在Modern C++中进行简洁、高效编程几乎是不可能的,尤其当你想偷懒进行某些通用编程的时候。

一、参数推演概述

  参数推演指的是编译器根据实参推导出相关模板参数的行为,这种行为不同于其他脚本类动态语言发生在运行时执行推演,而是在编译器编译的期间发生的。例如我们常见的对于函数模板的情形:

1
2
3
4
template<typename T>
void foo(ParamType param);
foo(expr);

  上面为模板函数定义形式,下面为实际调用表达式,在编译时候编译器需要推导出TParamType的具体类型,这里用两个名字表示是因为两者的类型通常是不相同的,因为ParamType通常还会带有一些额外的修饰符,比如:const、volatile、reference、pointer等。

1.1 模板参数推演规则

  (1) ParamType是指针或者引用类型,但是不是universe reference类型

此时如果expr是引用类型,则将引用的部分忽略掉;然后将上一步去除引用的剩余部分和ParamType进行匹配,以确定T的类型。
这里也就是意味着底层的const、volatile修饰符将会被保留。

  例如对于如下使用:

1
2
3
4
5
6
template<typename T>
void f(T& param){}
int x = 27; f(x); // T -> int, param -> int&
const int cx = x; f(cx); // T -> const int, param -> const int&
const int& crx = x; f(crx); // T -> const int, param -> const int&

  在面的例子里,即使模板参数的类型是T&,也可以传递给他一个const对象来调用,此时const的属性被推导至T参数中;最后一个调用在推导的时候首先会将引用剔除掉,所以得到的T类型和第二个调用是一样的。

C++11之统一初始化语法

  在当前新标准C++11的语法看来,变量合法的初始化器有如下形式:

1
2
3
4
X a1 {v};
X a2 = {v};
X a3 = v;
X a4(v);

  其实,上面第一种和第二种初始化方式在本质上没有任何差别,添加=则是一种习惯上的行为。使用花括号进行的列表初始化语法,其实早在C++98时代就有了,只不过历史上他们只是被用来对数组元素进行初始化操作,以及初始化自定义POD类型的数据(简单理解就是可以memcpy复制对象的类型)。比如:

1
2
3
int v1[] = {1, 2, 3, 4};
int v2[5] = {1,2,3};
char msg = "hello, world!";

  在使用列表来初始化数组的时候,如果声明数组的时候没有指定数组尺寸大小,则编译器就使用其列表包含的元素个数自动计算数组的尺寸;如果提供了数组尺寸,但是列表的元素数目小于数组尺寸,则系统会将剩余的元素全部赋值为0。如果是字符数组的话,C++还支持使用字符串常亮来进行初始化。

一、C++11的统一初始化器

  在新标准C++11中这个东西使用范围和特性被大大的扩展了,而且已经成为了一个基础而又重要的利器,几乎可以执行任何的初始化操作,所以也被称为”Uniform initialization”,尽管国内还是习惯上称为列表初始化。因为他可以避免传统初始化中的诸多问题和缺陷,所以从Bjarne Stroustrup爷爷的《C++ 程序设计语言》描述口吻看来,列表初始化是被大力推荐使用的,即便用惯旧式初始化的C++程序员初看起来会很不习惯,但C++强烈建议使用上述第一种方式进行统一初始化操作。

C++之CRTP手法

  初次相识CRTP就是boost::enable_shared_from_this,这种以派生类型作为模板参数生成基类的形式,让人看着很新奇。
  参阅了很多的文献,大多是讲使用CRTP可以实现编译器静态多态的效果,并与之同传统的虚函数实现多态进行对比,这里也类似的探究一下多态的特性。

一、C++的多态实现

1.1 传统的虚函数实现多态的原理

  我们通常讨论的C++之多态是通过虚函数的方式来实现的,其被称为运行时刻的多态(dynamic polymorphism, runtime polymorphism),因为程序运行时刻具体调用的函数实体是根据指针或者引用实际指向的实际对象类型来决定的,这就是C++的延迟绑定或者称为动态绑定的特性。
  在C++中,上述特性是通过vtable和vptr来实现的,比如某个基类Base具有function1和function2两个虚函数,并以其为基类得到两个派生类D1和D2,两个派生类D1和D2分别override了基类的function1和function2虚函数,这种关系下的vtable则形如下图所示:
vtable
  在C++中,每一个使用了虚函数的类(或者从含有虚函数的类派生出来的子类)都会拥有一个vtable,该vtable是在编译期间由编辑器产生的一个静态数组,该数组中的每个条目则指向了该类型可以访问的最接近的虚函数。这里最近可访问的函数就意味着,如果派生类override了基类的虚函数,则该vtable中的对应条目就指向派生类的虚函数,如果派生类没有override基类的虚函数,则该条目就直接指向其基类的虚函数版本。
  这里的vtable是与类类型相关的,而不针对于任何创建出来的具体对象,所有该类型的对象都共用该vtable,但是程序的执行依赖于创建的对象来运行的,所以编译器在创建vtable的同时还创建了vptr成员变量,该变量还会被所有的派生类所继承,当使用一个具有vtable的类型创建一个对象的时候,该vptr成员就指向了其具体对象类型的vtable。
  通过上面的这些操作,C++通过虚函数实现运行时绑定就显而易见了:程序在执行一个函数调用的时候,如果发现该函数是一个虚函数,则用其vptr成员访问其动态类型关联的vtable,然后在vtable中找到合适的虚函数版本进行实际的函数调用。不过经过上面的步骤,我们会发现使用虚函数机制,会让每个对象增加一个额外的vptr大小的开销(比如64位系统上就是8字节);同时在调用虚函数的时候还需要查找虚函数表去寻求正确的函数版本,所以会有人诟病虚函数机制会对函数调用性能产生负面影响;还有就是虚函数通常不能被inline,对于一些尺寸小的高频调用函数将无法得到内联的优化。

国庆假日回家所感

  因为需要回家办点事情,所以国庆法定休假日之前就请了三天假回去了。回家进门第一感觉就是爸妈长得没有过年时候好了,人明显消瘦了同时也变黑了,估计是爸爸在外打散工,妈妈在附近工艺厂熬夜做针织太辛苦了所致。其实现在他们的头发早已花白过半,但是还是每一段时间就回去把头发染黑,估计他们也知道这样的发色显然和自己的年龄不相匹配,也觉得白发出门显得很难为情吧。俗话说相由心生,爸妈心中一定也怀着满满的不安、承受着极大的压力。唉,整个社会除了那少部分先富起来的人之外,谁又能活得有多滋润呢?
  其实,自己基本每次打电话都会跟爸妈一而再再而三地嘱咐,他们已经上了年纪了,国家认定60岁法定应该退休了,所以他们还在勉强还在做体力活实在不适当的,尤其爸爸过年的时候说腰疼,去医院拍片后说是脊柱退化所致——毕竟机体开始衰老了,也上了年纪了,所以不应当再做体力活了。但是他们就像千千万万农村普通农民一样,一直都是那么的勤劳朴实,也一直不听我的劝告多停下来休息休息,因为在农村人看来没有子女相伴、儿孙绕膝都还没到享福的阶段,况且我自己现在工作生活还漂泊在外,工作性质和房价种种因素交错的原因,也无法在家乡安居陪伴在他们的身边,所以他们也总是想着在自己还能动一点的时候,尽量的多挣一点钱给我减轻一点压力。
  假期在家跟妈妈聊天的时候,发现老家很多看着我长大的长辈们一个个都不在了,因为自己常年在外读书和工作的原因,这些长辈有的很多年都没碰过面了,虽然年隔已久,但是只要想到他们的话,小时候和他们接触的印象还能活伶伶的在浮现在自己的脑袋中,只可惜物是人非,在之后的岁月中再也看不到他们了,虽说即便同我非亲非故,但向来多愁善感的我心中却不禁莫名难受起来。不过近年的境像就是,近年来家乡的人,很多都是六十多岁、甚至五十多岁就容易患上各种恶疾,比如中风、糖尿病、尤其是各种癌症,特别是在城镇化建设的进程中很多村民搬迁到集镇上面来开始的。更有一个好朋友告诉我,她们村的男丁很多都是年纪不大就走了,村子俨然都快成寡妇村了。当然,这种问题的原因也是总说纷纭,村里是各说各的:有人说是农村人搬到城镇上,没有活干了,生活习惯和生活方式的骤变带来各种不适应所致;有人说是现在的环境大不如前了,虽然城里的环境自然不如乡下山清水秀,尤其时常看电视听广播也知道现在的工业污染严重,大气污染、水污染已经严重威胁到大众的生活健康了;再有就是现在的城乡建设大兴土木,破坏了本地人的风水所致。

再说智能指针

  在之前的文章已经介绍过了现代C++最具价值的开发组件——智能指针了,这东西使用起来比较简单,而且大多数情况下也不容易用错,所以也不会去深究他。但是,最近看了Sutter和Meyer的相关文章和书籍后,对C++智能指针的使用有了更进一步的了解和认识。C++是一个讲求效能和务实的语言,如果本着这个观念去做事,那么C++的很多特性和组件都很有讲究,所以看见这些大神们的经验、总结和套路,虽然也有看不懂焦头烂额得时候,但一旦领略其中的奥妙,那种拨云见日,豁然开朗的感觉真的不可言喻。虽然相比于Python以及新秀Go,C++的复杂和深奥是出了名的,不过也正因为此,先比其他语言而言更具有折腾的潜质,其中的乐趣也只有体验过才会真正有所感受。

一、智能指针解析

  智能指针是用来管理堆上动态分配对象的利器,当然通过定制化析构的技巧,也可以管理其他类型的资源。

1.1 unique_ptr

  unique_ptr是一个独占类型的智能指针,其不允许其他的智能指针共享其内部管理的指针,所以该指针对象不允许被拷贝,但是允许通过函数被返回、通过std::move进行移动来转移其控制权。比较可惜的是C++11不支持std::make_unique方法,也可以自己简单的先实现一个:

1
2
3
4
template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&&... params) {
return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}

  unique_ptr在指定删除器的时候需要在创建unique_ptr对象的时候,指定删除器的类型信息作为模板参数(所以删除器类型是整个unique_ptr类型的一部分),而不能像shared_ptr那样直接指定删除器对象而不用指定删除器类型信息,甚至推迟在reset()的时候指定删除器,这是为了unique_ptr的高效而这样做的,可以产生更快的运行时代码。
  unique_ptr支持数组形式,但是应当避免使用它们,用容器来存储智能指针,而不要让智能指针指向数组。

生产环境软件的调试信息

  虽然把系统做的坚若磐石是每个程序员的理想和坚持不懈的目标,但往往现实就是现实,就是必须面对的东西。每当服务被引入新的组件或者特性的时候,即使在本地环境下各种测、各种压,但是上线后还是可能会遇到各种问题导致服务挂掉。这不,前两天刚引入的两个更改(syslog日志以及Redis队列服务解耦),导致核心服务crash了两次,当然在每天几十万笔交易中出现两次问题(分属两个特性),说明不是普遍性Bug,很有可能是在特定的条件、特定数据情况下才被触发的。
  线上的服务器打开了core dump的功能,这点还是要表扬的。出现任何问题都要保留尸体,只有这样才能追踪问题,不在相同的位置再跌倒第二次。不过线上运行的程序都是发行版,缺乏调试信息,所以用发行版的软件虽然能加载调用栈信息,知道是哪里出了问题,但是具体的调用参数就找不到了,而这种偶发性的问题大多都是因为调用参数的异常导致的,所以这种情况下的尸体是没法用的。
  使用-g编译过的程序,在可执行程序的体积和占用内存方面会耗费比较大,至于执行速度方面则不得而知了。
  网上搜了一下,针对生产版本软件调试符号的处理,主要有两个方法:(1) 编译带调试版本的软件,然后将调试符号strip调后,用于生产环境;(2) 编译不带调试版本的软件,然后记录其软件版本号,出了问题后现编带调试版本的软件进行调试。

一、记录版本号法

  主要是记录线上运行软件对应的软件分支和版本号,出了问题后将代码树checkout到指定的版本信息,然后编译出一个带调试符号的版本用于问题的跟踪调试。我们知道在Linux下,对于常亮字符串可执行区域有一个专门的区域用于保存这些字符串常量,通过strings工具可以取出这些字符串常量。

ZooKeeper C语言API简单试用

  ZooKeeper有官方标准C语言绑定客户端却没有C++版本的,反正身为一个见多识广的C++程序员对此已经习以为常、生无可恋了。其实,ZooKeeper最流行的客户端绑定是Curator,不过它是Java语言的,这个库极为的流行好用,于是乎GitHub上面有很多模仿Curator的接口,然后基于C语言绑定库,封装实现出C++语言的ZooKeeper客户端,不过貌似都是小作坊之手笔(没有太多的Star),所以也不敢直接上。
  C语言的客户端库咋看之下会比较的复杂,因为异步的方式将操作和回调割裂开来,代码会比较碎片化。通过XMind将这些接口梳理一下,其实也挺容易的。

一、ZooKeepr C语言客户端基本使用

  在之前介绍编译C客户端的时候,会同时生成C语言开发所需的库文件。我们通常会使用其多线程异步版本的库,该版本库在使用的时候,会自动创建单独的IO线程、事件线程用于处理连接和事件回调操作。
  下面,首先将zookeeper.h头文件中的重要函数接口罗列出来,其实对照前面一篇文章看来,这些函数的意义也是清晰可辨的,而且接口的风格比较统一。基本有些函数名会有一个额外添加w标记的版本(没办法,C语言不支持重载),可以用于设置watch event,当侦听的事件发生后收到notification时,就会使用事先设置参数去调用指定的回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ZOOAPI zhandle_t *zookeeper_init(...);
ZOOAPI int zookeeper_close(zhandle_t *zh);
ZOOAPI const clientid_t *zoo_client_id(zhandle_t *zh);
ZOOAPI int zoo_acreate(zhandle_t *zh, const char *path, ...);
ZOOAPI int zoo_adelete(zhandle_t *zh, const char *path, ...);
ZOOAPI int zoo_aexists(zhandle_t *zh, const char *path, int watch, ... );
ZOOAPI int zoo_awexists(zhandle_t *zh, const char *path, watcher_fn watcher, ...);
ZOOAPI int zoo_aget(zhandle_t *zh, const char *path, ...
ZOOAPI int zoo_awget(zhandle_t *zh, const char *path, watcher_fn watcher, ...);
ZOOAPI int zoo_aset(zhandle_t *zh, const char *path, ...);
ZOOAPI int zoo_aget_children(zhandle_t *zh, const char *path, ...);
ZOOAPI int zoo_awget_children(zhandle_t *zh, const char *path, watcher_fn watcher, ...);
ZOOAPI int zoo_async(zhandle_t *zh, const char *path, string_completion_t completion, ...);

  为了看起来更加的直观,同时也方便查找和备忘,我重要的五个函数整理到一个xmind的文件中了:
zookeeper

C++设计中的Handle处理类

  Handle这个内容是在看Andrew和Barbara夫妇的《C++沉思录》中接触到的,被广为翻译为句柄,用他来控制其所管理的类。刚开始看瞄的时候就觉得:握草,这不就是智能指针的原型么,难道是没有智能指针时代的轮子?但是耐着性子看完后,觉得还是收获不少的,起码的话算是对智能指针中引用计数、写时复制的设计实现写的比较清楚了。
  题外话,其实智能指正在我司也早有轮子,并且一直被使用至今了。今天过细看了下代码,发现都是最简单的Scoped非拷贝使用方式,而在需要外传的时候都是使用引用的方式传递出去,所以算是挂羊头卖狗肉吧,Shared类其实根本就没做到Shared的事儿,就是个简单的RAII做的事儿。不过,因为我们的业务逻辑比较简单,所以长久使用起来也没有什么问题……

一、简单实现

1.1 准备工作

  作者过于循循善诱的细节东西就不细说了,Point这个类是我们实际的用户类,跟业务相关的我们不管;Handle类是我们要实现的句柄类,我们的目的是要将Point的对象绑定到Handle对象上,让handle控制他所绑定的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Point {
public:
Point(int x, int y): x_(x), y_(y) {}
private:
int x_; int y_;
};
class Handle {
public:
Handle(int, int);
Handle(const Point& p);
private:
UPoint* up_; // TODO...
};

  为了用户使用的舒适傻瓜,那么Handle就应该自动接手用户类Point资源的创建和销毁,通过Handle的构造函数我们得知申请资源的时候就有两种方式:(1)直接让Handle类的构造函数参数和Point类构造函数签名一致,然后做一个参数转发;(2)用拷贝的方式,创建一个现有Point的对象的副本,而原始对象的资源我们不关心。

Linux下的日志服务Rsyslog

  当前公司的业务系统是自己搞了个日志服务,其实也就是对格式化后的消息使用UDP发送做了一个封装,然后日志服务端根据执行进程名字创建对应的日志文件后,对相同服务的日志内容的聚集和落盘操作,并且日志文件每日进行截断保存。由于一些异常交易的跟踪排查都通过日志文件进行跟踪,所以平时对日志文件的依赖还是比较大的,但使用过程中发现这个自研的日志系统问题多多:
  (1). 日志文件的内容经常会乱序,虽然根据时间戳可以判断日志的先后顺序,但人为跳来跳去总有些别扭。在多线程环境下每发一条日志都会创建一个UDP socket,所以预想在今后随着业务增长和线程数目增加,日志量攀升的情况下这种乱序现象会更为的严重;
  (2). 有时候本该有的日志没有发现,不知道是执行流程改了还是怀疑是日志本身丢失了,毕竟是UDP协议发送的,可靠性是得不到保障的;
  (3). 采用自定义的日志格式,虽然zabbix也能进行一些正则匹配,但是我想后续接入专业的日志分析处理系统可能会比较困难,这里把所有的日志元数据都格式化死到消息体里面的;
  (4). 日志消息没法按照level灵活的分文件存放,查阅日志需要在一大堆的调试信息中寻找,费神费力,所以效率真心堪忧。

  一句话三个字儿:坑爹啊!所以在此强烈建议规模不大研发能力又有限的公司,还是应该选择成熟、大众、主流的后台开发组件:一方面这些开源组件对你遇到的和将要遇到的坑,人家都遇到帮你填了;二来他们通常构架灵活,扩充和修改方便,而且围绕他们做的配套工具和组件周边也比较丰富,很多事情都不必自己再做了。虽然,对程序员来说造一个轮子很简单,通过造轮子对比也是一个很好的学习进步方式,更厉害的是轮子对公来说也是KPI亮点,但是没有严格论证测试就将其推向生产环境是一个极度不负责任的行为,情况严重的话会给后续的开发维护带来不小的负担。