C++11—可变参数模板
目录
概念
定义
参数包扩展
递归扩展
逗号表达式扩展
实际应用
string实现
概念
可变参数模板是C++11引入的,它允许模板的参数可以变化也就是说函数模板和类模板的参数是可变的。这种特性极大地增强了C++模板的灵活性和表达能力。这就类似于C语言的printf它也可以做到任意输入参数不同的是这是个函数并不是模板(这篇就只对可变参数的函数模板进行介绍)。
定义
它的写法和之前的函数模板很像就只是在class后和参数列表里加个...,具体如下:
template<class …Args>
返回类型 函数名(Args… args)
{
//函数体;
}
上面的args是参数包,参数包是用于表示一组可变数量的模板参数。
template<class...Args>
void Test(Args...args)
{
//...
}
参数包扩展
参数包的扩展即将包中的参数逐个取出或进行其他操作。大家可能会想到能不能用访问数组中元素的方式来进行扩展即args[i],实际上这并不可行,具体如下:
template<class...Args>
void Test(Args...args)
{
for (int i = 0; i < sizeof...(args); i++)
{
cout << args[i];
}
cout << endl;
}
int main()
{
Test(1, 2.2, string("11111"));
return 0;
上面的sizeof...是一个编译时运算符,用于获取一个参数包中元素的数量。要想正确进行扩展就要用到递归扩展、逗号表达式扩展方法。
递归扩展
虽然我们无法直接从参数包里拿到数据,但是我们可以在参数传递过程中拿到数据。我们可以再写一个可变参数的函数模板只不过它的参数列表是这样(T val, Args...args)。当传过去总的args(假设里面有N个参数)时。val会拿到它的第一个参数后面的args会拿到它的N-1个参数。就这样一直递归。当数据遍历完我们要重载一个无参的函数用来结束递归。
注意:虽然这里说的是递归扩展但并不是在运行时发生的而是在编译时进行递归推导。在使用参数包时...要写在args的后面并不是前面。具体如下:
void print() {}
template<class T, class...Args>
void print(T val, Args...args)
{
cout << val << ' ';
print(args...);
}
template<class...Args>
void Test(Args...args)
{
print(args...);
cout << endl;
}
逗号表达式扩展
执行逗号表达式时,会从左到右依次计算每个子表达式,但是逗号表达式的值是最后一个子表达式的值。前面的子表达式仅会被计算。例:
int a = (1,2,3);
上面的1,2都会被计算但都会被忽略。变量a最终会被赋值为3
。
这次实现我们依然是在参数传递过程中拿到数据。这次是在数组中利用逗号表达式来实现。根据逗号表达式的特性我们可以在第一个子表达式中调用扩展参数包的函数,第二个用来数组初始化。不要忘记在重载一个无参的函数这是用来解决无参的情况。具体如下:
void print() {}
template<class T>
void print(T val)
{
cout << val << " ";
}
template<class... Args>
void Test(Args... args)
{
int arr[] = { (print(args), 0)... };
cout << endl;
}
arr数组里的内容就相当于:
int arr[] = { (print(1), 0), (print(2.2), 0), (print(string("11111")), 0) };
实际应用
C++11以后的STL容器新增了emplace系列的接口,emplace系列的接口均为模板可变参数。功能上兼容push和insert系列,我的建议是以后尽量用emplace系列的接口。下面我会对push_back和emplace_back作比较来说明为什么(下面的举例是右值引用和可变参数模板的综合,没看的可以看看:右值引用)。
list的emplace_back如下:
可以发现这不仅仅是一个可变参数模板也是万能引用。下面我会用到我自己实现的string来当list里的实例化的类型,我把它放到文章最后了。
当用如下代码时:
int main()
{
list<zz::string> lt;
zz::string s1 = "11";
zz::string s2 = "22";
lt.emplace_back(s1);
lt.push_back(s2);
return 0;
}
结果:
可以看到有两个构造它们分别是s1,s2的。然后要创建新对象但又因为参数是左值无法进行移动所以这里就只能是拷贝构造。
当用如下代码时:
int main()
{
list<zz::string> lt;
zz::string s1 = "11";
zz::string s2 = "22";
lt.emplace_back(move(s1));
lt.push_back(move(s2));
return 0;
}
结果:
其原因和上面一样。它们是右值可以移动所以是移动构造。
当用如下代码时:
int main()
{
list<zz::string> lt;
lt.emplace_back("11");
cout << "**************" << endl;
lt.push_back("22");
return 0;
}
结果:
可以看到不一样了。对于push_back它能接受的类型是string而"22"的类型是const char*它们之间的类型不同,会进行隐式类型转换。在此过程中会构造一个临时变量,又因为临时对象是右值可移动所以就调用了移动构造。而对于emplace_back它可以接受const char*因为它是一个模板(push_back之所以不能是因为在类模板实例化之后它也就有了确定的类型了),所以这里就直接构造对象就行了。
如果要是浅拷贝的话这里的移动构造就要换成拷贝构造了。所以我建议以后用emplace系列就行了。
string实现
namespace zz
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
//构造
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str)-构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//移动构造
string(string&& s)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
//移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
//拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 拷贝构造" << endl;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
//拷贝赋值
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;
if (this != &s)
{
_str[0] = '\0';
_size = 0;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
return *this;
}
//析构
~string()
{
delete[] _str;
_str = nullptr;
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
if (_str)
{
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';
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
}