前言
本期我们接着继续介绍C++11的新特性,本期我们介绍的这个新特性是很多人都感觉抽象的语法!它就是可变的模板参数!
目录
一、可变的模板参数
1.1可变的参数列表
对于可变参数列表,我们在C语言就见过!例如最典型的就是 printf / scanf 了!
我们在使用的时候,需要指定类型将不同类型/数量的类型都可以传递过去!
int main()
{
int a;
double b;
char c;
printf("请输入一个整数、一个浮点数和一个字符: ");
scanf("%d %lf %c", &a, &b, &c);
printf("输入了: %d %lf %c\n", a, b, c);
return 0;
}
1.2可变的参数包
C++98/03,类模板和函数模板中只能含固定的模板参数,C++11引入了可变的模板参数能够让你创建接受可变参数的函数模板和类模板!这无疑是一个巨大的改进!正是改变巨大,所以这块比较抽象,也比较晦涩难懂!本博客主要介绍的是函数模板的可变参数!
先来见一见基本的可变参数的函数模板:
template <class ...Args>
void ShowList(Args... args)
{
// ...
}
此时,我们就可以给这个函数模板传递任意类型、任意个数的参数,把这个前面带 ... 的参数统称为参数包!Args是模板的参数包,args是函数形参的参数包(Args... args的个数是0~N个)!
一般为了提高传递参数的效率,可变参数的类型一般会被写成,万能引用(引用折叠)的形式:
template <class ...Args>
void ShowList(Args&&... args)
{
// ...
}
由于可变模板的参数是可变的,即支持任意类型、任意数量的参数,所以可以这样写:
int main()
{
ShowList(1);
ShowList(1, 'a', 9.9);
ShowList(1, "aaa", 9.9, 'b');
ShowList(std::vector<int>(), std::list<double>(), 'zzz', 999, "aaaaaaa");
return 0;
}
这也体现了可变模板参数的强大!
1.3可变参数包的解析
用可变参数的人爽了,但是解析参数包的过程其实是不简单的!下面是一种非常典型的错误栗子:
template<class ...Args>
void showList(Args... args)
{
// 错误的解析参数方式
int n = sizeof...(args);
for (int i = 0; i < n; i++)
{
// 获取具体的可变参数
args[i];
}
}
这里使用sizeof获取参数包args的大小是把...放到了args的前面,第一次看起来的时候有点别扭~!(个人感觉)
这种方式是符合我们的直觉的,但是他是错的!
这里简单理解就是编译器不支持这样写!sizeof是编译时操作符,而参数包不是和数组一样连续的,所以不可用下标访问!
• 递归展开解析
void ShowList() { cout << endl; }// 用于结束递归
template<class T, class ...Args>
void ShowList(const T& t, Args&& ...args)
{
cout << t << " ";
ShowList(args...);
}
这里他是如何做的呢?我用一个简单的例子,第三个来画一下:
也就是编译器,在编译期间将上述的函数模板实例化成了很多的实例,在运行时进行了调用!
• 逗号表达式解析
除了上述递归的展开的方式解析参数包以外,还有一种直接展开的方式,就是用逗号表达式解析!具体如下:
template<class T>
void Print(T& t)
{
cout << t << endl;
}
template<class ...Args>
void ShowList(Args&&... args)
{
int arr[] = { (Print(args), 0)... };
cout << endl;
}
OK,编译后他就是这个样子:
template<class ...Args>
void ShowList(Args&&... args)
{
int arr[] = { (Print(1), 0), (Print('a'), 0), (Print(9,9), 0)};
cout << endl;
}
为什么这里要写成 (Print(args), 0)... 呢?
我们知道,逗号表达式的结果是最后一项的值!这是主要是在展开的同时给arr数组赋值!
二、emplace系列接口
C++11 也在STL中引入了可变的模板参数,这一批接口被称为emlpace系列的接口/函数!
这里以list为例,进行介绍!
为了方便演示,我们玩还是使用我们前几期用的cp::string,所以我们把他给拿过来:
namespace cp
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str) -- 构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 拷贝构造" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 赋值拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
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
};
cp::string to_string(int value)
{
cp::string str;
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}
list<cp::string> lt;
// 分别让emplace_back和push_back都插入一个 左值 对象
cp::string s1("hello");
cout << "-------------------------------" << endl;
lt.push_back(s1);
cout << endl;
lt.emplace_back(s1);
// 分别让emplace_back和push_back都插入一个 左值move后的 对象
cout << "-------------------------------" << endl;
lt.push_back(move(s1));
cout << endl;
lt.emplace_back(move(s1));
// 分别让emplace_back和push_back都插入一个 右值 对象
cout << "-------------------------------" << endl;
lt.push_back(cp::string("6666"));
cout << endl;
lt.emplace_back(cp::string("6666"));
// 分别让emplace_back插入一个参数包 push_back都插入一个右值 对象
cout << "-------------------------------" << endl;
lt.push_back("6666");
cout << endl;
lt.emplace_back("6666");
看结果:
通过上面的栗子,我们可以得出结论:
1、当emplace和push系列都插入左值/move(左值)的结果是一样的(拷贝构造/移动构造)!
2、当emplace插入一个参数包,push系列都插入一个右值时,emplace是构造,而push系列是构造+移动构造!
• 为什么emplace系列传入参数包后是直接构造?
上面介绍了,emplace系列的是引用了模板的可变参数,所以他可以接受的是一个参数包!其实他的底层是一层层的传递下去,直接到构造那里,用参数包的参数直接构造的,所以就只有一次构造!
这其实就是,有人所说的emplace效率高的原因所在!就是直接使用emplace传递参数包可以少一次,移动构造!
所以,emplace系列的插入对象的优先级为:
参数包 > 右值 > 左值
这里左值的代价较高,因为有一次的拷贝构造!右值其实和参数包的差别也还好,原因是移动构造的待见很轻!但是能用参数包的地方还是用参数包!以后就建议多使用emplace系列,因为emplace系列>=push系列 的效率!
OK,好兄弟,本期分享就到这里!我是cp我们下期再见~!