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!,在编译期就已经被编译器计算出来了。

分支

在模板元编程中,分支结构的实现依赖于一个不太为人熟知的编译器特性:SFINAESFINAESubstitution 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的类型是intis_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阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352

推荐阅读更多精彩内容