c++进阶之----c++11(可变参数模板)
1.基本概念
可变参数模板允许模板函数或模板类接受零个或多个参数。这些参数可以是不同类型的数据。其语法的核心是使用 ...
(省略号)来表示可变参数部分。
2.语法
template <typename... Args>
void func(Args... args);
-
typename... Args
:表示参数类型列表,Args
是一个类型参数包(parameter pack)。 -
args...
:表示参数值列表,args
是一个值参数包(parameter pack)。 -
使用sizeof...运算符去计算参数包中参数的个数。
3.完美转发
完美转发是指在模板函数中,能够将参数以完全相同的值类别(lvalue 或 rvalue)传递给目标函数。std::forward
是实现完美转发的关键工具。
3.1为什么需要完美转发?
在模板函数中,参数的类型可能是一个左值引用或右值引用。如果不使用 std::forward
,直接将参数传递给目标函数可能会导致类型不匹配或性能问题。
-
如果直接传递参数,左值引用会被当作左值处理,右值引用会被当作右值处理。
-
但目标函数可能需要以不同的方式处理左值和右值。
3.2 std::forward
的工作原理
std::forward
的行为取决于模板参数 T
的类型:
-
如果
T
是一个左值引用类型(T&
),std::forward
会将参数作为左值返回。 -
如果
T
是一个右值引用类型(T&&
),std::forward
会将参数作为右值返回。 -
如果
T
是一个非引用类型(T
),std::forward
会根据传入的参数类型决定返回左值还是右值。
3.3 引用折叠
在C++中,引用折叠是C++11标准引入的一个特性,用于处理引用的引用(例如 T&&
)时的类型推导规则。引用折叠的目的是确保引用的引用不会导致复杂的、难以理解的类型,而是会折叠成一个简单的引用类型。它在模板编程和完美转发中非常重要。
3.3.1 引用折叠的规则
引用折叠的规则如下:
-
两个左值引用:
T& &
折叠成T&
。 -
左值引用和右值引用:
T& &&
折叠成T&
。 -
右值引用和左值引用:
T&& &
折叠成T&
。 -
两个右值引用:
T&& &&
折叠成T&&
。 -
总结:左+左=左;左+右=左;右+右=右
3.3.2 代码训练
判断一下代码哪些会报错(ps:先不要看注释,自己做,结合引用折叠的规则判断函数被实例化成什么,在根据左值右值判断正确与否
// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤
#include<iostream>
using namespace std;
template<class T>
void f1(T& x)
{}
// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤
//如果传入的参数是左值,T&&会折叠为T&,即左值引用。
//如果传入的参数是右值,T&& 保持为T&& ,即右值引用。
template<class T>
void f2(T&&x)
{}
int main()
{
typedef int& lref; //左值引用
typedef int&& rref; //右值引用
int n = 0;
lref& r1 = n; //左+左=左 // r1 的类型是 int&
lref&& r2 = n; //右+左=左 // r2 的类型是 int&
rref& r3 = n; //右+左=左 // r3 的类型是 int&
rref&& r4 = 1; //右+右=右 // r4 的类型是 int&&
//先通过f1f2的定义判断出被实例化成啥,在判断对错
// 没有折叠->实例化为void f1(int& x)
f1<int>(n); //因为 n 是一个左值(假设 n 是一个 int 类型的变量),它可以绑定到 int& 上。
//f1<int>(0); 这是因为 0 是一个整型字面量,它是一个右值,而 f1<int> 被实例化为 void f1(int& x),它期望的是一个 int 类型的左值引用。在C++中,你不能将一个右值绑定到左值引用上
// 折叠->实例化为void f1(int& x)
f1<int&>(n);
//f1<int&>(0); // 报错//因为 0 是一个右值,不能绑定到 int& 上。
// 折叠->实例化为void f1(int& x)
f1<int&&>(n);
//f1<int&&>(0); // 报错
// 折叠->实例化为void f1(const int& x)
//字面量可以绑定到 const 左值引用上
f1<const int&>(n);
f1<const int&>(0);
// 折叠->实例化为void f1(const int& x)
f1<const int&&>(n);
f1<const int&&>(0);
// 没有折叠->实例化为void f2(int&& x)
//f2<int>(n);
f2<int>(0);
// 折叠->实例化为void f2(int& x)
f2<int&>(n);
//f2<int&>(0); // 报错
// 折叠->实例化为void f2(int&& x)
//f2<int&&>(n); // 报错
f2<int&&>(0);
return 0;
}
3.4 万能引用
万能引用是一种特殊的引用类型,它在模板参数推导时可以匹配任何类型(左值或右值)。它使用 &&
来声明,但是它的行为取决于模板参数的推导结果。
示例:
// 万能引用
template<class T>
void function(T&& t)
{
int a = 0;
T x = a;
//x++;
cout << &a << endl;
cout << &x << endl << endl;
}
int main()
{
// 10是右值,推导出T为int,模板实例化为void Function(int&& t),不折叠
function(10);
int a;
// a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)
function(a);
// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t),不折叠
function(move(a)); 右值
const int b = 8;
// b是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int&t)
// const 限定的变量不能被修改,所以Function内部会编译报错,x不能++
function(b); // const 左值
// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)
// 所以Function内部会编译报错,x不能++
function(std::move(b)); // const 右值
return 0;
}
我们可以用forward优化一下
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 万能引用
template<class T>
void Function(T&& t)
{
// 保持t的属性
Fun(forward<T>(t));
}
int main()
{
// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(10); // 右值
int a;
// a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)
Function(a); // 左值
// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(std::move(a)); // 右值
const int b = 8;
// a是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int& t)
Function(b);
// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
Function(std::move(b)); // const 右值
return 0;
}
4. emplace接口
在C++中,emplace
系列接口是一组非常有用的函数,它们允许你直接在容器内部构造对象,而不是先构造一个临时对象然后再将其插入容器中。这可以提高效率,特别是当对象的构造函数比较复杂或者需要多个参数时。
4.1 主要特点
-
原地构造:
emplace
系列函数直接在容器内部构造对象,避免了额外的复制或移动操作。 -
可变参数模板:这些函数通常与可变参数模板一起使用,以便于直接在容器内部就地拷贝对象,而不是通过拷贝构造函数或移动构造函数进行操作。
-
性能优化:在某些情况下,使用
emplace
系列函数可以显著提升性能,尤其是当插入的对象较复杂时。 -
代码简洁:
emplace
提供了更简洁的语法,直接传递构造函数的参数,无需显式地创建对象。 -
构造方式:传左值,跟push_back一样,走拷贝构造;传右值,跟push_back一样,走移动 构造
-
使用方式:
emplace
系列接口的使用方式与容器原有的插入接口的使用方式类似,但又有一些不同之处。例如,emplace_back
函数可以接受用于构造元素的参数包。不要再写{}了!!!而push_back()要写,要走initializer_list
std::vector<std::pair<int, std::string>> mylist;
mylist.emplace_back(1, "A");
在这个例子中,我们直接传递了构造std::pair<int, std::string>
所需的参数,而不是先创建一个pair
对象再将其插入到vector
中。
4.2 与push_back的比较
4.2.1 push_back
当使用 push_back
时,如果容器需要更多的空间来存储新元素,可能会触发重新分配内存的操作,这涉及到复制或移动现有元素到新的内存位置,然后添加新元素。push_back
通常需要一个已经构造好的对象作为参数,或者使用拷贝构造函数或移动构造函数来构造新元素。
4.2.2 emplace_back
emplace_back
是 C++11 引入的方法,它允许你直接在容器内部构造新元素,而不需要先构造一个临时对象。使用 emplace_back
可以传递多个参数,这些参数将直接用于构造新元素,从而避免了额外的复制或移动操作。emplace_back
通常用于需要传递多个参数或避免临时对象的场合,特别是在处理大型对象或资源密集型对象时,可以显著提高性能。
4.2.3 性能比较
在大多数情况下,emplace_back
比 push_back
更有效率,因为它避免了创建和销毁临时对象的开销。当容器需要重新分配内存时,emplace_back
和 push_back
的性能差异可能不那么明显,因为重新分配的开销占主导地位。
4.2.4 使用场景
使用 push_back:
当你已经有一个构造好的对象,或者当你需要添加的对象的构造函数非常简单时。
使用 emplace_back
:当你需要传递多个参数来构造新元素,或者当你希望避免创建临时对象以提高性能时。