可变参数模板
- 什么是可变参数
- 模板的可变参数
- 展开参数包
- emplace系列函数
- 引例
- emplace系列函数
什么是可变参数
printf和scanf中就涉及可变参数
这里三个点就代表可变参数,意思就是不管你传多少个参数,都可以接收
printf("%d",x);
printf("%d%d",x,y);
printf("%d%d%d",x,y,z);
模板的可变参数
C++98中类模版和函数模版中只能含固定数量的模版参数。C++11中新增可变参数模板能够让您创建可以接受可变参数的函数模板和类模,相比
样例:
template <class ...Args>
void ShowList(Args... args)
{}
- 形参args前面有…,所以args就是一个可变模板参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0) 个模版参数
- Args是一个模板参数包,args是一个函数形参参数包(这里的Args和args就是个名称,一般都叫这个)
使用示例:
展开参数包
我们无法直接通过args[i]
这样方式获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。
下面介绍两种展开参数包的方式:
- 第一种:递归函数方式展开参数包
//递归终止函数:当参数包中没有参数时,走此函数,结束递归
void _showlist()
{cout << endl;
}//当参数包中参数个数不为0时,走此函数,进行递归
template<class T, class ...Args>
void _showlist(T value , Args... args)
{cout << value << " ";_showlist(args...);
}//该函数能够接收任意参数个数的参数包
template< class ...Args>
void showlist(Args... args)
{_showlist(args...);
}int main()
{showlist();showlist(1);showlist(1,2);showlist(1,2,2.2);showlist(1,2,2.2,"xxx"); return 0;
}
首先,showlist
函数可以接收任意实参个数,进入参数包args中;然后根据参数包args中参数的个数调用_showlist
当参数包中参数个数大于0时,就调用void _showlist(T value , Args... args)
,每调用一次这个函数,参数包中的参数个数就减1(原参数包中第一个参数给到value,剩下的参数进入新的参数包),直至参数包中参数个数为0时,调用void _showlist()
,递归结束
这种方式就是利用递归函数不断将形参参数包一个一个的拆解下来
- 第二种:逗号表达式展开参数包(看看就好,抽象得很)
template <class T>
void PrintArg(T t)
{cout << t << " ";
}//参数包个数为0时
void ShowList()
{cout << endl;
}//展开函数
template <class ...Args>
void ShowList(Args... args)
{int arr[] = { (PrintArg(args), 0)... };cout << endl;
}int main()
{ShowList();ShowList(1);ShowList(1, 'A');ShowList(1, 'A', std::string("sort"));return 0;
}
这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。
这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。expand函数中的逗号表达式:(printarg(args), 0)
,也是按照这个执行顺序,先执行printarg(args)
,再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——列表初始化,通过列表初始化来初始化一个变长数组, {(printarg(args), 0)...}
将会展开成{(printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc... }
,最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]
。
由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)
打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包
emplace系列函数
可变参数模板最常见的使用场景就是:emplace系列的函数
引例
我们先看一个可变参数模板的使用引例
class Date
{
public:Date(int year = 1, int month = 1, int day = 1):_year(year),_month(month),_day(day){}
private:int _year;int _month;int _day;
};template<class ...Args>
Date* Create(Args... args)
{Date* ret = new Date(args...);return ret;
}int main()
{Date* p1 = Create();Date* p2 = Create(2023);Date* p3 = Create(2023,9);Date* p4 = Create(2023,9,27);Date d(2023, 2, 2);Date* p5 = Create(d);return 0;
}
上述代码中:
-
p1-p4分别向参数包中传了0至3个参数,而这四种情况都可以成功匹配到Date的全缺省构造函数
-
p5传了一个Date类对象进入参数包,而这种情况会匹配Date类中的默认拷贝构造函数
这就是可变参数模板的优势,使得传参非常的灵活,会自动根据参数包中的参数去匹配最适合的函数
emplace系列函数
- 场景一:
我们以list为例:std::list< std::pair<int, char> > mylist;
定义了一个list,节点的中的值类型为pair
当我们想尾插一个节点时:
-
以前会这样写:
mylist.push_back(make_pair(40,'d'));
或者用c++11的列表初始化mylist.push_back({40,'d'});
,这两者本质上是一样的。 -
有了emplace系列函数后,可以这样写:
mylist.emplace_back(40,'d');
不管是push_back
还是emplace_back
其实参类型都应该是pair,所以上述两种写法的不同之处在于pair对象的产生。如同开头的Date引例一样,第一种产生pair对象是拷贝构造,而第二种产生pair对象是直接构造
在这种场景下,emplace_back
和push_back
效率上其实差不了多少
- 场景二:
还是以list为例:std::list< std::pair<int, xy::string> > mylist;
这里pair的第二个参数类型是string(涉及深拷贝的自定义类型)
当我们想尾插一个节点时:
- 以前:
mylist.push_back(make_pair(30, "sort"));
- 用emplace:
mylist.emplace_back(10, "sort");
对比发现:第一种由于是拷贝构造生成的pair类型,因此在拷贝时会调用移动拷贝去拷贝string;第二种调用的是pair的构造函数,因而省去了移动拷贝
但这里也并不能体现出,emplace系列的优越,因为前者也就比后者多了个移动构造,但移动构造的代价其实很低
- 场景三:
真要说emplace_back比push_back更高效,在内置类型上体现更明显
还是以list为例:std::list<Date> mylist
当进行尾插时:
- 第一种:
mylist.push_back(Date(2023,9,27));
- 第二种:
mylist.emplace_back(2023,9,27);
第一种:是构造+拷贝构造,因为push_back的类型必须是Date。所以这里是先将Date构造出来,然后往下传,传到list_node那里new Node
时就是拷贝构造
第二种:可以把参数包一直往下传,传到list_node那里new Node
时直接依据参数包匹配Date类中的构造函数来初始化Date类型的对象
此外,第一种只能传日期类对象;第二种既可以传日期类对象,还可以传参数包