C++11 标准库源代码剖析:连载之一

C++模板元编程

元程序一词来源于英文单词metaprogram。在英语中,metaprogram的意思是a program about a program,翻译过来就是程序的程序。说白了,元程序就是用于操纵代码的程序。

这听上去多少有点玄妙,其实你没少和元程序打交道,你用的C++编译器就是一个元程序,它操纵C++代码来生成汇编语言或机器码。

C++元编程不是被发明出来的,而是被无意中发现的。上个世纪九十年代末,某些爱钻研的小伙伴无意中发现C++模板可以用于编写元程序,刚开始他们只是编写元程序在编译期执行一些数值计算,后来更进一步发现除了数值计算,还可以通过模板在编译期执行类型计算。更进一步的研究发现,编译期类型计算有强大的威力,可以极大地提高代码的运行效率,于是模板元编程作为一种programming paradigm在C++社区流行开来。

促使模板元编程大行其道的最重要的原因就是效率。后面我们会看到,在模板元编程中,一些运行期的工作被转移到了编译期,也就是说程序在运行时会跑得更快。没有什么能阻止C++程序员对效率的向往,虽然模板元程序的源代码如天书般难懂,但是C++社区还是热情地拥抱这一新的programming paradigm,大量采用元编程的类库被开发出来,而C++ 11标准库几乎全部构建在元编程基础之上。所以,要看懂标准库源代码,首先就要懂一点元编程。

C++模板元编程常用技巧

元程序是在编译期由编译器直接解析并执行的。在编译期,编译器只能做整数值计算和类型计算,这就导致了元程序的代码结构和我们熟知的代码结构,即运行时代码结构,有很大区别:

  • 运行时代码结构通常包括常量、变量、数据结构、类、函数、循环,分支等;
  • 元程序中只能出现常量(包括整形常量和布尔型常量)、循环和分支结构。


常量

元程序中可以出现的常量包括整形常量布尔型常量,为了配合编译器强大的类型计算能力,整形常量通常都被定义为某种类型,这就是C++ 11中的integral_constant

整形常量

// file: type_traits

template <typename T, T v>
struct integral_constant {
    static constexpr T value =    v;
    typedef T                     value_type;
    typedef integral_constant     type;
    
    // ...
};

integral_constant的定义虽然很简单,但是意义却很重要。它将一个数值包装成为一个对象,这从一个侧面揭示模板元编程的真谛:编译期类型计算。

integral_constant的使用方法很简单,比如你可以这样写:

typedef integral_const<int, 2> two_type;
typedef integral_const<int, 6> six_type;

static_assert(two_type::value * 3 == six_type::value, "2*3 != 6");

当然,上面的代码并不必直接写

static_assert(2 * 3 == 6, "2*3 != 6");

更有意义,甚至更繁琐。这里只是举个例子说明它的用法,后面我们会看到integral_constant的各种用法。

布尔型常量

同整形常量一样,布尔型常量也有对应的元类型:true_typefalse_type,这两个类其实是integral_constanttypedef

// file: type_traits

typedef std::integral_constant<bool, true>  true_type;
typedef std::integral constant<bool, false> false_type;

循环

说完了常量,我们来说说循环。在模板元编程中,你得忘掉你最喜欢的for循环,因为在编译期,编译器只有一种循环方式:递归。下面的代码展示了通过模板实现递归循环的正确方式:

template<size_t N>
struct factorial {
    static constexpr size_t value = N * factorial<N-1>;
};

// 递归必须要有结束条件,一般用一个特化的模板来作为结束条件
template<>
struct factorial<1> {
    static constexpr size_t value = 1;
};

这段代码计算某个数的阶乘,比如要计算并输出5!,你可以这样写:

std::cout << factorial<5>::value << std::endl; // 20

需要指出的是,上面这行语句虽然在运行期才能看到结果,但实际的值,也就是5!,在编译期就已经被编译器计算出来了。

分支

在模板元编程中,分支结构的实现依赖于一个不太为人熟知的编译器特性:SFINAE。 SFINAESubstitution Failure Is Not An Error的首字母缩写。意思是说,当编译器在解析模板时,如果模板参数匹配失败,编译器不会报错,而是默默地跳过这段代码,继续编译后面的代码。举个例子:

template<class T>
typename T::multiplication_result multiply(T t1, T t2) {
    return t1 * t2;
}

long multiply(int i, int j){
    return i * j;
}

int main() {
    multiply(1, 2);
}

当编译器看到main()中的multiply(1, 2)的时候,需要在前面定义的两个multiply中挑出一个匹配的,编译器很有可能会选中这个:

template<class T>
typename T::multiplication_result multiply(T t1, T t2) {
    return t1 * t2;
}

问题是int::multiplication_result并不存在,这会导致编译错误,但由于SFINAE规则的存在,编译器会忽略这个错误,转而匹配第二个multiply函数。最终第一个multiple会被编译器扔掉,就像代码中从来不存在这样一个函数一样。

后面我们还会看到,SFINAE规则是标准库中很多type trait存在的基础。在这里我们只讲SFINAE规则的一个具体应用:实现类似于if...else的分支结构。

在标准库中有一个模板类enable_if,定义如下:

// file: type_traits

template<bool B, class T = void>
struct enable_if {
};

template<class T>
struct enable_if<true, T> {
    typedef T type;
};

如果Btrue,则enable_if<T>::type就是存在的,否则enable_if<T>::type就不存在。那它怎么用呢?我们来看个例子:

#include <iostream>
#include <type_traits>

using namespace std;

// #1
template<class T>
typename enable_if<is_pointer<T>::value, void>::type
do_something(T) {
    cout << "calling do_something(T*), return nothing\n";
}

// #2
template<class T>
typename enable_if<!is_pointer<T>::value, T>::type
do_something(T t) {
    cout << "calling do_something(T), return " << t << endl;
    return t;
}

int main() {
    int i = 3;
    do_something(i);
    do_something(&i);
}

输出

calling do_something(T), return 3
calling do_something(T*), return nothing

可见,当调用do_something(i)的时候,因为i的类型是int,is_pointer<int>::value的值为false,于是enable_if<is_pointer<T>::value, void>::type不存在,第一个do_something会导致一个匹配失败,于是编译器转而去匹配第二个do_something函数。同理可以知道do_something(&i)会匹配第一个do_something函数。总的来说,上面的代码相当于于一个如下的if...else语句:

if T is a pointer {
    // do something
}
else {
    // do something else
}


总结

整形常量,布尔常量,循环,分支,这基本就是元编程的全部要素了。很简单吧,你觉得它难懂,是因为你还不熟悉它的代码结构,一旦你熟悉了,也就没什么了。

最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352

推荐阅读更多精彩内容

  • 在C++11中,我们还是会看到一些新元素。这些新鲜出炉的元素可能会带来一些习惯上的改变,不过权衡之下,可能这样的改...
    认真学计算机阅读 5,474评论 1 27
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,644评论 18 139
  • 曾经听过这么一个故事: 从前,有个生麻疯病的人,病了近40年,一直痛苦地躺在路旁,等人把他带到有神奇力量的水池边。...
    寇廷聚阅读 209评论 0 0
  • 甩衣服的空当里随手点了一国产电影,两个原因:一是因为在主题图片里看到了孙红雷和陈建斌;二是因为看到了“喜剧”二字。...
    骑兵lady阅读 463评论 0 1
  • 我这三天都被要求带teeny。 昨晚9点多我就带着她睡觉了,可是她昨晚不是很乖。 今早7点起床到现在她去吃饭了我才...
    Ken_E阅读 214评论 0 0