C++11新增可变参数模板
C++11新增可变参数模板
- c语言的可变参数
- c++11新增可变参数模板及递归展开参数包
- 逗号表达式展开参数包
- STL中的emplace接口
- 为容器添加emplace_back
c语言的可变参数
在c语言,scanf
和printf
的形参就是一个可变参数。可变参数可以接受几乎无限个参数。
c语言阶段,使用可变参数:
- 需要展开
stdarg.h
头文件。 - 声明函数时给一个固定参数,用于确定可变参数的起始位置。
- 使用
va_list
类型定义一个变量,用于访问可变参数列表。 - 使用
va_start
初始化va_list
,绑定到固定参数。 - 使用
va_arg
按照指定类型读取va_list
的参数。 - 使用
va_end
清理va_list
。
例如定义一个能对任意数量的数据求和的函数:
#include<stdio.h>
#include<stdarg.h>int sum(int cnt, ...) {int ans = 0;va_list args;va_start(args, cnt);for (int i = 0; i < cnt; i++)ans += va_arg(args, int);va_end(args);return ans;
}int main() {printf("%d\n", sum(2, 2, 3));//第1个实参表示多少个参数printf("%d", sum(3, 2, 3,4));return 0;
}
再例如模拟printf
的c语言代码。
#include <stdio.h>
#include <stdarg.h>// 自定义的printf风格函数
void my_printf(const char* format, ...) {va_list args;va_start(args, format);//初始化可变参数列表vprintf(format, args);//使用vprintf输出va_end(args);//清理可变参数列表
}int main() {my_printf("Hello, %s! The answer is %d.\n", "world", 42);return 0;
}
关于可变参数如何实现,以后有机会再整理细节。
c++11新增可变参数模板及递归展开参数包
C++11新增的新特性中有可变参数模板,通过它可以创建接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。
下面就是一个基本可变参数的函数模板。
template <class ...Args>
void ShowList(Args... args)
{}
Args
是一个模板参数包,args
是一个函数形参参数包
声明一个参数包Args...args
,这个参数包中可以包含 0 到任意个模板参数。
参数args
前面有省略号,所以它就是一个可变模版参数,通常把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)
个模版参数。
若使用sizeof
计算可变参数包的大小,只能得到参数个数,而且还需要按照这个格式获取:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;template <class ...Args>
void ShowList(Args... args) {cout << sizeof...(args) << endl;//输出上传的形参个数
}int main() {ShowList("sekai", 3.14, 2.71);return 0;
}
语法设计上无法直接获取参数包args
中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]
这样方式获取可变参数,所以需要用一些奇招来一一获取参数包的值,比如编译时的递归推演。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;// 递归终止函数
template <class T>
void ShowList(const T& t) {cout << t << endl;
}// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args) {cout << value << " ";ShowList(args...);
}int main() {ShowList(1);ShowList(1, 'A');ShowList(2, 'A', std::string("sort"));ShowList("sekai", 3.14,2.71);return 0;
}
参数包可以为0,所以也能这样:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;// 递归终止函数
void ShowList() {//可变参数包内的参数为0cout << endl;
}// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args) {cout << value << " ";ShowList(args...);
}int main() {ShowList(1);ShowList(1, 'A');ShowList(2, 'A', std::string("sort"));ShowList("sekai", 3.14, 2.71);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
数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;template <class T>
void PrintArg(T t) {cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args) {int arr[] = { (PrintArg(args), 0)... };cout << endl;
}int main() {ShowList(1);ShowList(1, 'A');ShowList(2, 'A', std::string("sort"));ShowList("sekai", 3.14, 2.71);return 0;
}
但本质都是将参数包内的所有参数作为形参调用函数PrintArg
,因此可以直接让PrintArg
返回0,来初始化数组arr
即可。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;template <class T>
int PrintArg(T t) {cout << t << " ";return 0;
}
//展开函数
template <class ...Args>
void ShowList(Args... args) {//这种做法是让编译器强行解析参数包int arr[] = { PrintArg(args)... };//args有多少参数,就调用多少次PrintArgcout << endl;
}int main() {ShowList(1);ShowList(1, 'A');ShowList(2, 'A', std::string("sort"));ShowList("sekai", 3.14, 2.71);return 0;
}
STL中的emplace接口
STL的大多数容器都添加了emplace
。emplace
就用到了可变参数,还支持万能引用。例如emplace_back
尾插:
template <class... Args>
void emplace_back (Args&&... args);
后文用到的 mystd.h,string
(添加有移动构造和移动赋值):
#pragma once
#include<string>
#include<iostream>
#include<vector>
#include<cstdlib>
#include<typeinfo>
#include<cstring>
#include<cassert>
#include<algorithm>
using std::vector;
using std::string;
using std::cout;
using std::endl;
using std::reverse;
using std::forward;namespace mystd {class string {public:typedef char* iterator;iterator begin() {return _str;}iterator end() {return _str + _size;}string(const char* str = ""):_str(nullptr), _size(strlen(str)), _capacity(_size) {cout << "string(char* str) 构造\n";_str = new char[_capacity + 1];strcpy(_str, str);}// s1.swap(s2)void swap(string& s) {std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}// 拷贝构造string(const string& s):_str(nullptr), _size(0), _capacity(0) {cout << "string(string& s)拷贝构造\n";string tmp(s._str);swap(tmp);}// 移动构造string(string&& s):_str(nullptr), _size(0), _capacity(0) {cout << "string(string&& s)移动构造\n";swap(s);}// 赋值重载string& operator=(const string& s) {cout << "string& operator=(string&s) 赋值重载\n";if (this != &s) {char* tmp = newchar[s._capacity + 1];strcpy(tmp, s._str);delete[] _str;_str = tmp;_size = s._size;_capacity = s._capacity;}return *this;}//移动赋值string& operator=(string&& s) {cout << "string& operator=(string&& s) 移动赋值\n";swap(s);return *this;}~string() {delete[] _str;_str = nullptr;}char& operator[](size_t pos) {assert(pos < _size);return _str[pos];}void reserve(size_t n) {if (n > _capacity) {char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void push_back(char ch) {if (_size >= _capacity) {size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}//string operator+=(char ch)string& operator+=(char ch) {push_back(ch);return *this;}const char* c_str() const {return _str;}private:char* _str;size_t _size;size_t _capacity;//不包含最后做标识的\0};//因为ret是局部对象,不能传引用返回。//string& to_string(int x) mystd::string to_string(int x) {mystd::string ret;while (x) {int val = x % 10;x /= 10;ret += ('0' + val);}reverse(ret.begin(), ret.end());return ret;}
}
emplace_back
支持可变参数,若容器存储的数据类型是自定义类型,则拿到构建对象的参数后自己去创建对象插入容器。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<list>
#include"mystd.h"
using namespace std;int main() {//先查看库里的用法std::list<std::pair<mystd::string,int> >lt;lt.emplace_back("12345",1);//可变参数接受完所有参数就构造pair//lt.emplace_back("asdfg",1,"hjkl",2);//可变参数也不接受多余参数cout << endl;lt.push_back(make_pair("6789", 2));//需先构造pair再上传return 0;
}
1个参数时在用法上,和push_back
没什么太大的区别。真正的区别是emplace_back
拿完参数后直接构造,而push_back
要先构造string
对象(隐式类型转换),再移动构造。
但移动构造本身成本就很低,所以在库中emplace_back
比push_back
的优势也只是少调用一次或几次构造函数,影响不是特别明显。
为容器添加emplace_back
emplace_back
需要有支持万能引用的构造函数才能通过new(args...)
生成新的数据。
例如自制的list
,向其中添加的emplace_back
和支持万能引用的list
结点构造函数:
//支持万能引用的list结点构造函数
template<class... Args>
list_node(Args&&... args):_data(args...), _next(nullptr), _prev(nullptr) {}//自制emplace
template <class... Args>
void emplace_back(Args&&... args) {//直接生成数据的原理Node* newnode = new Node(args...);// 链接节点//可为insert增加一个可变参数版本,这里直接手动链接Node* cur = end()._node;Node* prev = cur->_prev;// prev newnode curprev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;
}
将函数加进自制的list
及其组件中。
namespace mystd {template<class T>struct list_node {T _data;list_node<T>* _next;list_node<T>* _prev;list_node(const T& x = T()):_data(x), _next(nullptr), _prev(nullptr) {}list_node(T&& x):_data(move(x)), _next(nullptr), _prev(nullptr) {}//支持万能引用的list结点构造函数template<class... Args>list_node(Args&&... args):_data(args...), _next(nullptr), _prev(nullptr) {}};// T T& T*// T cosnt T& const T*template<class T, class Ref, class Ptr>struct __list_iterator {typedef list_node<T> Node;typedef __list_iterator<T, Ref, Ptr> self;Node* _node;__list_iterator(Node* node):_node(node) {}self& operator++() {_node = _node->_next;return *this;}self& operator--() {_node = _node->_prev;return *this;}self operator++(int) {self tmp(*this);_node = _node->_next;return tmp;}self operator--(int) {self tmp(*this);_node = _node->_prev;return tmp;}Ref operator*() {return _node->_data;}Ptr operator->() {return &_node->_data;}bool operator!=(const self& s) {return _node != s._node;}bool operator==(const self& s) {return _node == s._node;}};template<class T>class list {public:typedef list_node<T> Node;typedef __list_iterator<T,T&, T*> iterator;typedef __list_iterator<T,const T&, const T*>const_iterator;const_iterator begin() const {return const_iterator(_head->_next);}const_iterator end() const {return const_iterator(_head);}iterator begin() {//return iterator(_head->_next);return _head->_next;}iterator end() {//return iterator(_head->_next);return _head;}void empty_init() {_head = new Node;_head->_next = _head;_head->_prev = _head;_size = 0;}list() {empty_init();}// lt2(lt1)list(const list<T>& lt) {empty_init();for (auto e : lt){push_back(e);}}void swap(list<T>& lt) {std::swap(_head, lt._head);std::swap(_size, lt._size);}// lt3 = lt1list<int>& operator=(list<int> lt) {swap(lt);return *this;}~list() {clear();delete _head;_head = nullptr;}void clear() {iterator it = begin();while (it != end()){it = erase(it);}}void push_back(const T& x) {insert(end(), x);}void push_back(T&& x) {//forward<T>可以保持x的左、右值引用属性insert(end(), forward<T>(x));}template <class... Args>void emplace_back(Args&&... args){Node* newnode = new Node(args...);// 链接节点Node* cur = end()._node;Node* prev = cur->_prev;// prev newnode curprev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;}void push_front(const T& x) {insert(begin(), x);}void pop_front() {erase(begin());}void pop_back() {erase(--end());}iterator insert(iterator pos, const T& x) {Node* cur = pos._node;Node* newnode = new Node(x);Node* prev = cur->_prev;// prev newnode curprev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;return iterator(newnode);}iterator insert(iterator pos, T&& x) {Node* cur = pos._node;Node* newnode = newNode(forward<T>(x));Node* prev = cur->_prev;// prev newnode curprev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;return iterator(newnode);}iterator erase(iterator pos) {Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;delete cur;prev->_next = next;next->_prev = prev;--_size;return iterator(next);}size_t size() {return _size;}private:Node* _head;size_t _size;};
}
再次测试list
:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<list>
#include"mystd.h"
using namespace std;int main() {mystd::list<std::pair<mystd::string,int> >lt;lt.emplace_back("12345",1);cout << endl;lt.push_back(make_pair("6789", 2));//需先构造pair再上传return 0;
}
输出:
string(char* str) 构造
string(string& s)拷贝构造
string(char* str) 构造
string(char* str) 构造string(char* str) 构造
string(string&& s)移动构造
emplace_back
对浅拷贝的情况效率会有提升。特别是成员多一些的那种,浅拷贝的类只有构造 +++ 拷贝构造,是直接的资源交换。
但实际上有了右值拷贝后,深拷贝的情况下emplace_back
和传统的insert
、push_back
比起来效率提升不是很明显。使用哪个接口取决于需求。