C++ 泛型编程(一) —— 可变参数模板

可变参数模板函数

可变参数模板是 C++ 11 中引入的一个新特性,它允许我们定义一个可以接受可变数目参数的模板函数或模板类。

在了解模板函数和模板类之前,我们需要先知道两个概念:

  • 模板参数包:使用 typename/class ... Args 来指出 Args 是一个模板参数包,其中可能包含了零个或多个模板参数
  • 函数参数包:使用 Args ... rest 来指出 rest 是一个函数参数包,其中可能包含了零个或多个函数参数

一个典型的可变参数模板定义如下:

template <typename ... Args>
void func(Args& ... rest){
    /* 函数体 */
}

与一般的模板相同,当编译器遇到可变参数模板函数的调用时,会根据调用时所传递的实参来推断模板参数类型以及包中参数的数目。例如:

int i = 10;
double pi = 3.14;
string str = "hello world!";
func(i, pi, str);   //包中含有 3 个参数
func(pi, str);      //包中含有 2 个参数
func(str);          //包中含有 1 个参数
func();             //包中含有 0 个参数(空包)

根据上述调用方式,编译器会为 func 实例化出以下四个不同版本

void func(int&, double&, string&);
void func(double&, string&);
void func(string&);
void func();

包扩展

对于一个可变参数函数模板而言,我们无法直接获取参数包中的参数,只能通过展开参数包的方式来访问参数包中的所有参数。C++ 中允许我们对参数包做执行两种操作:

  • 获取包大小:通过 sizeof... 运算符获取参数包中的参数个数,例如:

    template <typename ... Args>
    void g(Args ... args){
        cout << sizeof...(Args) << endl;
        cout << sizeof...(args) << endl;
    }
    
  • 包扩展:将一个包分解为构成的元素,并对每个元素应用模式,获得扩展后的列表。C++ 通过在模式的右边加上一个省略号来触发包扩展,例如:

    template <typename T, typename... Args>
    ostream& print(ostream &os, const T &t, const Args&... rest){
      os << t << ", ";
      return print(os, rest...);
    }
    

在 print 函数当中,我们进行了两次包扩展操作。第一次扩展操作扩展了模板参数包 Args, 编译器将模式 const Args& 应用到了 Args 中的每个元素,最终的结果式得到了一个用逗号分隔的零个或多个类型的参数列表。例如:

int i = 10;
double d = 3.14;
string str = "hello world";
print(cout, i, d, str); 
//将 ostrea& print(ostream&, const T&,const Args&...)扩展为: ostream& print(ostream &os, const int&, const double&, const string&);

第二次扩展则是发生在 print 的递归调用中,此时模式为函数参数包的名字 rest。该模式扩展出一个由包中元素组成并用逗号分隔的列表,因此最终的效果相当于: print(cout, d, str)

扩展参数包的两种方法

方法一:递归扩展

扩展一个参数包最常见的方法是递归。既然用到了递归,自然就免不了递归体和递归终止条件。例如:

//递归终止条件
template <typename T>
ostream& print(ostream &os, const T &t){
    return os << t;
}

//递归体
template <typename T, typename... Args>
ostream& print(ostream &os, const T &t, const Args&... rest){
    os << t << ", ";
    return print(os, rest...);
}
int main(void){
    int i = 10;
    double d = 3.14;
    string str = "hello world";
    print(cout, i, d, str); 
    return 0;
}

在递归体函数中,我们将函数参数包的首个元素打印出来,然后利用剩余参数调用自身,直到最后当参数包为空时,调用非可变参数版本的 print 函数退出递归。

注意:在使用递归方式进行包扩展时,将非可变参数版本 (递归终止条件) 必须要声明在可变参数版本 (递归体) 的作用域当中,否则会导致无限递归。!!

这是因为当非可变参数版本的函数声明在可变版本的作用域中时,在执行递归终止条件时会进行函数匹配,根据特例化原则会优先考虑调用可变参数版本。若将非可变参数版本的声明放到可变参数版本的作用域之外,则在执行递归终止条件时只会匹配到可变参数版本,从而造成无限递归。

方法二:利用逗号表达式来扩展

包扩展的第二种方法则是借助逗号表达式和初始化列表来实现。还是以前面的 print 函数为例,使用逗号表达式和初始化列表来实现:

template <typename T>
void print(ostream& os, const T &t){
    os << t << " ";
}
template <typename... Args>
void expand(ostream& os, Args&&... args){
    initializer_list<int>{(print(os, std::forward< Args>(args)),0)...};
}
int main(void){
    expand(cout, 1, 3.14, "hello world");
    return 0;
}

逗号表达式能够按顺序执行逗号前面的表达式,并返回逗号后边的值。例如:

d = (a = b, b);     //先执行 a = b,然后将 b 返回给 d

因此,expand 的函数体的功能

initializer_list<int>{(print(os, std::forward< Args>(args)),0)...}; //定义一个长度为 sizeof...(Args) 的整型数组,并统统初始化为 0。在初始化的同时,执行 print 函数

若使用 C++ 14 中的泛型 lambda,则可以将 print 函数转换为 lambda 表达式,使得代码更加简洁,如下:

template <typename F, typename... Args>
void expand(const F& f, ostream& os, Args&&... args){
    initializer_list<int>{(f(os, std::forward< Args>(args)),0)...};
}
int main(void){
    expand([](ostream& os, auto i){os << i << " ";}, cout, 1, 3.14, "hello world");
    return 0;
}

两种方法的优缺点

递归包扩展方式:

优点:实现更加灵活,我们可以针对递归终止条件进行不同于递归体函数的操作。
缺点:
1. 递归函数会反复压栈弹栈,因此运行时会消耗更多资源
2. 若递归终止条件没有声明在递归体的作用域内,则会导致无限循环。(不过所幸的是编译器可以检查出这样的问题。)

就地包扩展方式:

优点:执行的效率高于递归的方式
缺点:
1. 只能适用于对参数包中的每一个参数都执行相同操作的场景。
2. 浪费了一部分的内存空间,构造出来的初始化列表没有任何作用。

参考资料:

  1. 《C++ Primer》

  2. 泛化之美--C++11可变模版参数的妙用

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