我们在C++11(2)中已经很好的解释了右值引用,这次来看看右值引用剩余的一些话题:可变参数包与emplace_back。
目录
可变参数模板:
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。
现阶段我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们点到为止。
下面就是一个基本可变参数的函数模板:
c
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
可变参数的sizeof:
我们有时需要知道传入的参数个数,于是我们可以使用sizeof进而得知。
示例:
cpp
template<class ...Args>
void ShowList(Args... args)
{
cout << sizeof...(args) << endl;
}
int main()
{
ShowList(1, "你好", 4);
return 0;
结果:
可变参数的展开:
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为"参数包",它里面包含了0到N(N>=0)
个模版参数。
我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。
由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。
递归函数方式展开参数包:
cpp
// 递归终止函数
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(1, 'A', std::string("sort"));
return 0;
}
具体该怎么理解?
当我们调用函数传参时,第一个模板参数T会自动推演出参数包第一个参数,这样args这个参数包就少了一个参数,并继续调用自己,每次都会少第一个参数,就这样递归的推演直到遇到终止函数(即只剩一个参数时,因为编译器总会自动选择最匹配的函数进行调用)。
逗号表达式展开参数包:
这个比较奇葩。
这种展开参数包的方式,不需要通过递归终止函数,是直接在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数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包
cpp
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(1, 'A', std::string("sort"));
return 0;
}
注意:我们的可变模板参数都是在编译时推演出来的具体参数,所以我们也可以结合以下代码更好的理解逗号展开参数包。
cpp
void ShowList(char a1, char a2, std::string a3)
{
int arr[] = { (PrintArg(a1), 0),
(PrintArg(a2), 0),
(PrintArg(a3), 0),};
cout << endl;
}
emplace_back:
注意到我们的emplace_back也是使用了可变模板参数,但是使用了万能引用,这样大大传值返回的开销。
我们上述进行展开参数包时也都没有使用万能引用。
另外,还有一个说法就是empalce普遍比push系列效率优秀,这又是怎么一回事?
我们来看看:
对于深拷贝的类:
首先对于深拷贝我们还是最好有一个确切的例子才能更好的理解:
下段代码是一个简单实现string的模拟类
cpp
namespace cyc
{
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(string&& s)
:_str(nullptr)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// s1 = 将亡值
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)
{
push_back(ch);
return *this;
}
string operator+(char ch)
{
string tmp(*this);
tmp += ch;
return tmp;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
}
无区别:
当我们创建一个list时,尾插一个string对象这是无区别的,原因在于
emplace
时如果直接提供目标对象,则会调用拷贝构造或者移动语义
进行资源的创建或者转移,在这里由于传递的是左值,因此会构造新的对象
cpp
std::list<cyc::string> lt;
// 深拷贝:
// 无区别
cyc::string s1("111");
lt.push_back(s1); // 拷贝构造
lt.emplace_back(s1);// 拷贝构造
对于此的结论:两者都是深拷贝,无区别。
我们再来传入右值试一试:
可以看到仍没有区别,两者都是调用移动语义。
有区别:
当传入的都是一个常量字符串时呢?
emplace
时当传入的是一个目标对象所需的构造参数时,会在最后调用一个定位new
,直接构造.
对于浅拷贝的类:
对于深拷贝来说优化不是特别明显,但是对于浅拷贝来说,且很大的一个类时,emplace的性能就很棒了。
我们假设有一个日期类。
无区别 &&有区别:
cpp
// 浅拷贝的类
// 没区别
std::list<Date> list2;
Date d1(2023, 5, 28);
list2.push_back(d1);
list2.emplace_back(d1);
cout << "-----------------------------------------" << endl;
Date d2(2023, 5, 28);
list2.push_back(move(d1));
list2.emplace_back(move(d2));
// 有区别
cout << "-----------------------------------------" << endl;
list2.push_back(Date(2023, 5, 28));
list2.push_back({ 2023, 5, 28 });
cout << "-----------------------------------------" << endl;
list2.emplace_back(Date(2023, 5, 28)); // 构造+移动构造
list2.emplace_back(2023, 5, 28); // 直接构造
注意:这里对于可变参数模板传参有一个特点,不能使用initializer_list进行传参,因为本质上初始化列表是一个伪容器
,除了迭代器就没有方法得到其中的元素了,而我们的参数包想要的是一个一个分离的参数,initializer_list会被认为是一个整体,编译器不能正确的将其解析为一个一个元素,故不支持。
而vector等STL容器就不一样了,他们是真正的容器。
虽然技术上可以实现,但是为了哲学性,严谨性等原因还是没有这样做。
完~~