有些时候,我们定义一个函数,可能这个函数需要支持可变长参数,也就是说调用者可以传入任意个数的参数。比如C函数printf().

我们可以这么调用。

printf("name: %s, number: %d", "Obama", );  

那么这个函数是怎么实现的呢?其实C语言支持可变长参数的。

我们举个例子,

double Sum(int count, ...)
{
va_list ap;
double sum = ; va_start(ap, count); for (int i = ; i < count; ++i)
{
double arg = va_arg(ap, double);
sum += arg;
} va_end(ap); return sum;
}

上面这个函数,接受变长参数,用来把所有输入参数累加起来。可以这么调:

double sum = Sum(, 1.0, 2.0, 3.0, 4.0);  

计算结果是10,很好。

 那么C语言的这个函数有什么问题呢?

  • 1. 函数本身并不知道传进来几个参数,比如我现在多传一个参数,或者少传一个参数,那么函数本身是检测不到这个问题的。这就可能会导致未定义的错误。
  • 2. 函数本身也不知道传进来的参数类型。以上面的例子,假如我把第二个参数1.0改成一个字符串,又如何?答案就是会得到未定义的错误,也就是不知道会发生什么。
  • 3. 对于可变长参数,我们只能用__cdecl调用约定,因为只有调用者才知道传进来几个参数,那么也只有调用者才能维持栈平衡。如果是__stdcall,那么函数需要负责栈平衡,可是函数本身根本不知道有几个参数,函数调用结束后,根本不知道需要将几个参数pop out。(注:某些编译器如VS,如果用户写了个__stdcall的可变长参数函数,VS会自动转换成__cdecl的,当然这是编译器干的事情)

在C++语言里面,在C++11之前,C++也只是兼容了C的这种写法,而C++本身并没有更好的替代方案。其实对于C++这种强类型语言而言,C的这种可变长方案等于是开了个后门,函数居然不知道传进来的参数是什么类型。

所以在C++11里面专门提供了对可变长参数的更现代化的支持,那就是可变长模板。

模板参数包(template parameter pack)

template<typename... A> class Car;  

typename... 就表示一个模板参数包。可以这么来实例化模板:

Car<int, char> car; 

再来看一个更加具体的例子:

template<typename T1, typename T2> class Car{};
template<typename... A>
class BMW : public Car<A...>{};
BMW<int, char> car;

在这个例子里面,BMW是一个可变参数的模板,它继承于类Car. 那么BMW<int, char> car;在进行模板推导的时候,可以认为变成Car<int, char>了。这其中的功劳应该属于A...

A... 称之为包扩展(pack extension),包扩展是可以传递的。比如继承的时候,或者直接在函数参数里面传递。然后当编译器进行推导的时候,就会对这个包扩展进行展开,上面的例子,A...就展开成了int, char。

C++11定义了可以展开包的几个地方:

  • 1. 表达式
  • 2. 初始化列表
  • 3. 基类描述列表
  • 4. 类成员初始化列表
  • 5. 模板参数列表
  • 6. 通用属性列表
  • 7. lamda函数的捕捉列表

其他地方是不能展开的。

针对上面的例子,如果我们改成BMW<int, char, int> car, 会如何呢?编译的时候就直接报错了,

Error 1  error C2977: 'Car' : too many template arguments d:\study\consoleapplication2\variablelengthparameters\variablelengthparameters.cpp27 1 VariableLengthParameters

这是因为当展开的时候,A...变成了int, char, int了,可能基类根本就没有3个模板参数,所以推导就出错了。

那如果这样的话,可变长参数还是啥意义呢?这等于每次的参数个数还是固定的啊。当然不会这么傻,其实C++11可以通过递归来实现真正的可变长的。看下面的代码。

//////////////////////////////////////////////////////////////
#include <typeinfo>
// 可变参数模板类:通过继承+偏特化 展开参数包
template<typename... A> class BMW
{
public:
BMW()
{
cout << "--------------------1" << endl;
}
}; // 模板特化
template<typename Head, typename... Tail>
class BMW<Head, Tail...> : public BMW<Tail...> //
{
public:
// BMW<int, char, float> 总会优先调用基类的构造函数,所以打印顺序float->char->int
BMW()
{
cout << "type: " << typeid(Head).name() << endl;
}
private:
Head head;
}; template<> class BMW<>
{
public:
BMW()
{
cout << "--------------------2" << endl;
}
}; // 递归边界条件 int mutableTemplateParamTest()
{
// 编译时期即可确定参数个数和类型
BMW<int, char, float> mycar1;
} //output:
//--------------------2
//type: f
//type: c
//type: i

//////////////////////////////////////////////////////////////
// 可变参数模板类:通过模板偏特化和递归方式展开参数包
template<typename First, typename... Rest>
class Sum
{
public:
    enum { value = Sum<First>::value + Sum<Rest...>::value };
};


template<typename Last>
class Sum<Last>
{
public:
    enum{ value = sizeof(Last) };
};


//Sum<int, int> obj;
//cout << obj.value << endl;
//cout << Sum<int, int>::value << endl;
//output:
//8
//8

如果我们运行这段代码,会发现构造函数被调用了3次。第一次得到的类型是float,第二次是char,第三次是int。这就好像模板实例化的时候层层展开了。实际上也就是这么一回事情。

这里使用了C++模板的特化来实现了递归,每递归一次就得到一个类型。看一下对象car里面有什么:

可以清晰的看到car里面有三个head。基类里面的head是float,第二个head是char,第三个head是int。

有了这个基础之后,我们就可以实现我们的可变长模板类了,std::tuple就是个很好的例子。可以看看它的源代码,这里就不再介绍了。

可变长模板不光可以用于类的定义,也可以用户函数模板。接下来,就用可变长参数来实现一个Sum函数,然后跟上面的C语言版本做对比。

可变长模板实现Sum函数(通过递归方式展开参数包)

直接看代码:

template<typename T1, typename... T2> 
double Sum2(T1 p, T2... arg)
{
double ret = p + Sum2(arg...);
return ret;
} double Sum2() // 边界条件
{
return ;
}

在上面的代码里面,可以很清楚的看到递归。

double ret2 = Sum2(1.0, 2.0, 3.0, 4.0);  

这条调用代码同样得到结果10。这样过程可以理解为,边界条件的函数先执行完毕,然后4.0的执行完毕,再3.0,2.0,1.0以此被执行完毕。一个典型的递归。

ok,那么跟C语言版本相比,又有哪些好处呢?

变长模板优点

之前提到的几个C语言版本的主要缺点:

  • 1. 参数个数:那么对于模板来说,在模板推导的时候,就已经知道参数的个数了,也就是说在编译的时候就确定了,这样编译器就存在可能去优化代码。
  • 2. 参数类型:推导的时候也已经确定了,模板函数就可以知道参数类型了。
  • 3. 既然编译的时候就知道参数个数和参数类型了,那么调用约定也就没有限制了。

来实验一下第二点吧

int _tmain(int argc, _TCHAR* argv[])
{
double ret1 = Sum(, 1.0, 2.0, 3.0, 4.0, "abcd");
double ret2 = Sum2(1.0, 2.0, 3.0, 4.0, "abcd");
return ;
}

Sum是C语言版本,最后一个参数传了个字符串,但是Sum函数是无法检测这个错误的。结果也就是未定义。

Sum2是个模板函数,最后一个参数也是字符串,在编译的时候就报错了,

Error 1  error C2111: '+' : pointer addition requires integral operandd:\study\consoleapplication2\variablelengthparameters\variablelengthparameters.cpp29 1 VariableLengthParameters

double无法和字符串相加,这样在编译的时候就告诉我们这个错误了,我们就可以修复它,但是C语言的版本不会报错,代码也就失控了,不知道会得到什么结果。

怎么样,变长模板比C语言的变长参数好一些吧。

所以,我们还是尽可能使用C++11的变长模板吧。

最后一个问题,为什么使用变长参数呢?有些人可能会问,是不是可以把所有的参数放到一个list里面,然后函数遍历整个list,再相加呢?good point,

如果所有的参数类型都一样,确实可以这么做,但是如果参数类型不一样呢?那怎么放到一个list里面?像C++这种强类型语言可能做不到吧,确实弱类型语言比如php,python等,确实可以这么做。

根据我的理解,脚本语言等弱类型语言不需要变长参数吧,或者不重要。但是C++还是需要的,用可变长模板就没这个问题了,就算参数类型不一样,只要对应的类型有对应的操作,就没问题。

当然像上面的例子,如果没有重载+, 那么编译的时候就报错,这不就是我们需要的吗?

附:

// VariableLengthParameters.cpp : Defines the entry point for the console application.
#include "stdafx.h"
#include "stdarg.h"
#include <typeinfo> double Sum(int count, ...)
{
va_list ap;
double sum = ; va_start(ap, count);
for (int i = ; i < count; ++i)
{
double arg = va_arg(ap, double);
sum += arg;
}
va_end(ap); return sum;
} template<typename T1, typename... T2> double Sum2(T1 p, T2... arg)
{
double ret = p + Sum2(arg...);
return ret;
} double Sum2()
{
return ;
} template<typename... A> class BMW{}; template<typename Head, typename... Tail>
class BMW<Head, Tail...> : public BMW<Tail...>
{
public:
BMW()
{
printf("type: %s\n", typeid(Head).name());
}
Head head;
}; template<> class BMW<>{}; BMW<int, char, float> car; int _tmain(int argc, _TCHAR* argv[])
{
double ret1 = Sum(, 1.0, 2.0, 3.0, 4.0);
double ret2 = Sum2(1.0, 2.0, 3.0, 4.0); return ;
}

【转自】http://blog.csdn.net/zj510/article/details/36633603

C++11新特性之五——可变参数模板的更多相关文章

  1. JDK5新特性之 可变参数的方法

    可变参数的方法:不知道这个方法该定义多少个参数 注意: > 参数实际上是数组 > 必须写在参数列表最后一个 package cn.itcast.day24.varparam; import ...

  2. jdk1.5新特性之------->可变参数

    /* jdk1.5新特性之------->可变参数 需求: 定义一个函数做加法功能(函数做几个数据 的加法功能是不确定). 可变参数的格式: 数据类型... 变量名 可变参数要 注意的细节: 1 ...

  3. JDK5新特性:可变参数方法

    JDK1.5增加可变参方法,其定义格式为: 访问修饰符 返回值类型 方法标识符(参数类型 参数标识符1,参数类型 参数标识符2,参数类型...参数标识符){} 如可能要定义一个求和功能的方法,但求和的 ...

  4. JDK5的新特性之可变参数&Arrays.asList()方法

    [代码] package com.hxl; import java.util.Arrays; import java.util.List; public class Test { public sta ...

  5. java新特性之可变参数

    public class NewDemo01 {     public static void main(String[] args) {         System.out.print(" ...

  6. JDK5 新特性之 可变参数的方法(2)---asList

    > Arrays.asList(T - a)方法的使用 >UnsupportedOperationException分析     Arrays.asList(T - a)方法的使用 pac ...

  7. [C++11]C++可变参数模板

    可变参数模板 原文链接: http://blog.csdn.net/xiaohu2022/article/details/69076281 普通模板只可以采取固定数量的模板参数.然而,有时候我们希望模 ...

  8. [转载] C++11新特性

    C++11标准发布已有一段时间了, 维基百科上有对C++11新标准的变化和C++11新特性介绍的文章. 我是一名C++程序员,非常想了解一下C++11. 英文版的维基百科看起来非常费劲,而中文版维基百 ...

  9. c++ 11 线程池---完全使用c++ 11新特性

    前言: 目前网上的c++线程池资源多是使用老版本或者使用系统接口实现,使用c++ 11新特性的不多,最近研究了一下,实现一个简单版本,可实现任意任意参数函数的调用以及获得返回值. 0 前置知识 首先介 ...

随机推荐

  1. JS学习笔记(6)--音乐播放器

    说明(2017.3.15): 1. lrc.js里面存储LRC歌词的格式的数组,获取里面的时间轴,转为秒数. 2. 通过audio.currentTime属性,setinterval每秒获取歌曲播放的 ...

  2. Hive UDF IP解析(一):依赖包兼容性问题

    Java依赖环境: <dependency> <groupId>org.apache.hive</groupId> <artifactId>hive-e ...

  3. OpenResty(Nginx+Lua)开发入门

    Nginx入门 本文目的是学习Nginx+Lua开发,对于Nginx基本知识可以参考如下文章: nginx启动.关闭.重启 http://www.cnblogs.com/derekchen/archi ...

  4. history统计命令最多的20条

    1.1.1 统计使用命令最多的20条 [root@ob1 ~]# history|awk '{ml[$2]++}END{for (i in ml) print i,ml[i]}'|sort -nrk ...

  5. 历届蓝桥杯C/C++省赛试题

    2012年第三届蓝桥杯C/C++程序设计本科B组省赛 2013年第四届蓝桥杯C/C++程序设计本科B组省赛 2014年第五届蓝桥杯C/C++程序设计本科B组省赛 2015年第六届蓝桥杯C/C++程序设 ...

  6. jqueryEasyui常用代码

    //查询: function doSearch(form){ var fields =$('#queryForm').serializeArray(); var $fm = $(form); var ...

  7. BOOTH 算法的简单理解

    学习FPGA时,对于乘法的运算,尤其是对于有符号的乘法运算,也许最熟悉不过的就是 BOOTH算法了. 这里讲解一下BOOTH算法的计算过程,方便大家对BOOTH的理解.        上图是BOOTH ...

  8. 几种常见的DIV边框样式

    <html> <head> <title>边框样式</title> </head> <body> <p style=bor ...

  9. ROS 教程之 vision : 用各种摄像头获取图像

    可能有很多人想在ROS下学习视觉,先用摄像头获取图像,再用opencv做相应算法处理,可是ROS下图像的采集可不像平常的read一下那么简单,需要借助外部package的使用.而摄像头即可以用笔记本自 ...

  10. linux profile\bashrc\bash_profile之间的区别和联系

    /etc/profile 每个用户,首次登录时被执行: /etc/bashrc 每个运行bash shell的用户都执行此文件,当bsh被打开时,该文件被读取: ~/.bash_profile 专用于 ...