C++11 可变参数模版、lambda表达式、包装器与部分新内容添加
- 一、可变参数模版
- 基本语法及原理
- 包扩展
- emplace系列接口
- 二、lambda表达式
- lambda表达式语法
- 捕捉列表
- lambda的原理
- lambda的应用
- 三、包装器
- bind
- function
- 四、部分新内容添加
- 新的类功能
- 1.默认的移动构造和移动赋值
- 2.声明时给缺省值
- 3.defult 和 delete
- 4.final 与 override
- STL中一些变化
以下代码环境为 VS2022 C++。
一、可变参数模版
基本语法及原理
C++11 支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包。
存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包,表示零或多个函数参数。
我们用省略号来指出一个模板参数或函数参数表示一个包。在模板参数列表中,class… 或 typename… 指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟 … 指出接下来表示零或多个形参对象列表。
函数参数包可以用左值引用或右值引用表示,跟上一篇文章讲的普通模板一样,每个参数实例化时遵循引用折叠规则。
#include <iostream>
#include <vector>
using namespace std;template<class ...Args>
class one
{;
};template<class ...Args>
void get(Args... args)
{;
}template<class ...Args>
void set(const Args&... args)
{;
}template<class ...Args>
void print(Args&&... args)
{;
}int main()
{get(2, "111111", 5 + 6);set(5.1, 'h', 'e');print(1.5, 5.1f, "haha");print("hehe");one<int, double, long long, char, float> get1;one<string, vector<long long>> get2;one<long long> get3;return 0;
}
可变参数模板的原理跟模板类似,本质还是在编译时实例化对应类型和个数的多个函数。
这里可以使用 sizeof… 运算符去计算参数包中参数的个数。
#include <iostream>
using namespace std;template<class ...Args>
void print(Args&&... args)
{cout << sizeof...(args) << endl;
}int main()
{print(1, 2, 3); // 3个print(1.5, 5.1f, "haha"); // 3个print(); // 0个print("hehe"); // 1个return 0;
}
包扩展
对于一个参数包,除了能计算它的参数个数,我们能做的唯一的事情就是扩展它,当扩展一个包时,我们还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放一个省略号 … 来触发扩展操作。
#include <iostream>
using namespace std;void print() // 当参数为 0 时自动匹配无参 print 递归终止
{cout << endl;
}template<class T, class ...Args>
void print(T&& x, Args&&... args)
{cout << x << " "; // 递归调用 print ,将接收到的第一个参数进行打印print(args...); // 用包扩展将剩下的参数传给下一个 print
}// 这里用 print(1.5, 5.1f, "haha"); 为例
// 首先会实例化 void print(double x, float y, string z) 函数
// 先打印 x,再通过包扩展将 y, z 传给下一个 print
//void print(double x, float y, string z)
//{
// cout << x << " ";
// print(y, z);
//}
// 下一个实例化为 void print(float y, string z) 函数
// 先打印 y,再通过包扩展将 z 传给下一个 print
//void print(float y, string z)
//{
// cout << y << " ";
// print(z);
//}
// 下一个实例化为 void print(string z) 函数
// 打印 z,此时包扩展里的参数为 0,会匹配 print() 函数结束包扩展
//void print(string z)
//{
// cout << z << " ";
// print();
//}int main()
{print(1, 2, 3);print(1.5, 5.1f, "haha");print();print("hehe");return 0;
}
C++11 还支持更复杂的包扩展,直接将参数包依次展开作为实参给一个函数去处理。
#include <iostream>
using namespace std;template<class T>
int show(T&& x)
{cout << x << " ";return 0;
}template<class ...a>
void get(a&& ...x)
{;
}template<class ...Args>
class one
{
public:one() = default;
};template<class ...Args>
void print(Args&&... args)
{// 展开发生的位置一般在函数参数列表、成员初始化列表、属性列表、类模版参数列表等// show(args)... 表示每个参数都会调用一次 show()// 若有三个参数可表示为// show(one), show(two), show(three)// int arr[] = { 0, show(one), show(two), show(three) };// get(show(one), show(two), show(three));int arr[] = { 0, show(args)... }; // 数组执行顺序是从左到右//get(show(args)...); // 函数参数执行是从右到左one<Args...> get; // 类型可在类模板参数列表展开cout << endl;
}int main()
{print(1, 2, 3);print(1.5, 5.1f, "haha");print();print("hehe");return 0;
}
emplace系列接口
C++11 以后 STL 容器新增了 emplace 系列的接口,emplace 系列的接口均为模板可变参数,功能上兼容 push 和 insert 系列,但是 emplace 还支持新操作,假设容器为container<Type>,emplace 支持直接传入构造 Type 对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造 Type 对象。
emplace 系列相对于 insert 和 push 系列总体而言是更高效,可以平替后两者。
#include <iostream>
#include <vector>
using namespace std;int main()
{vector<pair<string, int>> arr;pair<string, int> get1 = { "1111111111", 1 };// 传左值一样走拷贝构造arr.push_back(get1);arr.emplace_back(get1);// 传右值一样走移动构造arr.push_back(pair<string, int>("22222222", 5));arr.emplace_back(pair<string, int>("22222222", 5));// push_back 会先 有参构造 再 移动构造arr.push_back({ "333333333", 10 });// emplace_back 会将参数直接传进去进行有参构造,没有移动构造,此时效率高一点arr.emplace_back("333333333", 10);return 0;
}
二、lambda表达式
lambda表达式语法
lambda 表达式本质是一个匿名函数对象,跟普通函数不同的是它可以定义在函数内部。
lambda 表达式语法使用层而言没有类型,所以一般是用 auto 或者 模板参数定义的对象 去接收 lambda 对象。
lambda表达式的格式: [capture-list](parameters)->return type { function body }
[capture-list] : 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据 [] 来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用,捕捉列表可以传值和传引用捕捉。捕捉列表为空也不能省略。
(parameters) :参数列表,与普通函数的参数列表功能类似,但如果不需要参数传递,则可以将 () 省略。
->return type :返回值类型,用追踪返回类型形式去声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{ function body } :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
#include <iostream>
using namespace std;int main()
{auto add = [](int x, int y)->int { return x + y; };cout << add(10, 20) << endl;// 1. [] 不能省略// 2. () 可以省略// 3. ->return type 可以省略// 4. {} 不能省略auto func1 = [] {};func1();auto swap = [](int& one, int& two)->void{int temp = one;one = two;two = temp;};int a = 100;int b = 1;cout << "a == " << a << endl << "b == " << b << endl << endl;swap(a, b);cout << "a == " << a << endl << "b == " << b << endl;return 0;
}
捕捉列表
lambda 表达式中默认只能用 lambda 函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉。
- 第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。[ x, y, &z ] 表示 x 和 y 值捕捉,z 引用捕捉。
#include <iostream>
using namespace std;int main()
{int x = 0;int y = 10;int z = 100;int t = 1000;auto func1 = [x, y, &z]{cout << x << " " << y << " " << z << endl;};func1();return 0;
}
- 第二种捕捉方式是在捕捉列表中隐式捕捉,在捕捉列表写一个 “ = ” 表示隐式值捕捉,在捕捉列表写一个 “ & ” 表示隐式引用捕捉,这样 lambda 表达式中用了哪些变量,编译器就会自动捕捉哪些变量。
#include <iostream>
using namespace std;int main()
{int x = 0;int y = 10;int z = 100;int t = 1000;auto func1 = [=]{cout << "func1 " << x << " " << y << " " << z << endl;};auto func2 = [&]{cout << "func2 " << x << " " << y << " " << z << endl;};func1();func2();return 0;
}
- 第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。当使用混合捕捉时,第一个元素必须是 “ & ” 或 “ = ”,并且 “ & ” 混合捕捉时,后面的捕捉变量必须是值捕捉,同理 “ = ” 混合捕捉时,后面的捕捉变量必须是引用捕捉。[ =, &x ] 表示其他变量隐式值捕捉,x 引用捕捉;[ &, z, y ] 表示其他变量引用捕捉,z 和 y 值捕捉。
#include <iostream>
using namespace std;int main()
{int x = 0;int y = 10;int z = 100;auto func1 = [=, &x]{cout << "func1 " << x << " " << y << " " << z << endl;};auto func2 = [&, z, y]{cout << "func2 " << x << " " << y << " " << z << endl;};func1();func2();return 0;
}
lambda 表达式如果在函数局部域中,它可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉,在 lambda 表达式中可以直接使用。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
默认情况下, lambda 捕捉列表是被 const 修饰的,则传值捕捉的过来的对象不能修改,mutable 加在参数列表的后面可以取消其常量性,使用该修饰符后,传值捕捉的对象就可以修改了,但是修改的是形参对象,不会影响实参。使用该修饰符后,参数列表即使为空也不可省略。
#include <iostream>
using namespace std;string str1 = "全局变量";auto func1 = [] // 定义在全局位置,捕捉列表必须为空{cout << str1 << endl;cout << "hello world!" << endl;};void test()
{string str4 = "另一个函数中的局部变量";static string str5 = "另一个函数中的静态局部变量";
}int main()
{string str2 = "局部变量";static string str3 = "静态局部变量";auto func2 = [&str2]{cout << str1 << endl;cout << str2 << endl;cout << str3 << endl;//cout << str4 << endl; // 无法使用其他局部域变量//cout << str5 << endl; // 无法使用其他局部域静态变量};func1();func2();int a = 10;int b = 20;int c = 30;int d = 40;cout << endl << a << " " << b << " " << c << " " << d << endl;auto func3 = [a, b, &c, &d]{//a = 0; // 不可修改//b = 0;d = c = 1000;};func3();cout << a << " " << b << " " << c << " " << d << endl;// 传值捕捉是拷贝,并被 const 修饰,// mutable 可以去掉 const 属性,使得值捕捉变量可修改auto func4 = [a, b, &c, &d]()mutable ->void{a = 0; // mutable 修饰后可修改,但不会影响外面被值捕捉的值b = 0;d = c = -99;};func4();cout << a << " " << b << " " << c << " " << d << endl;return 0;
}
lambda的原理
lambda 的原理和 范围for 很像,编译后从汇编指令层的角度看,压根就没有 lambda 和 范围for 这样的东西。范围for 底层是迭代器,而 lambda 底层是仿函数对象,也就说如果我们写了一个 lambda 以后,编译器会生成一个对应的仿函数的类。
仿函数的类名是编译器按一定规则生成的,保证不同的 lambda 生成的类名不同。
lambda 参数 / 返回类型 / 函数体 就是仿函数 operator() 的 参数 / 返回类型 / 函数体, lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,如果隐式捕捉,使用哪些就传哪些对象。
lambda的应用
一般使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对也会比较麻烦。使用 lambda 去定义可调用对象,既简单又方便。
lambda 在很多地方用起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等。
三、包装器
bind
std::bind 是一个函数模板,它是一个可调用对象的包装器,可以把它看做一个函数适配器,对接收的可调用对象进行处理后返回另一个可调用对象。 std::bind 可以用来调整参数个数和参数顺序。
std::bind 在 <functional> 这个头文件中。
调用 std::bind 的一般形式: auto newCallable = std::bind(callable, arg_list);其中 newCallable 本身是一个可调用对象,arg_list 是一个逗号分隔的参数列表,对应给定的 callable 的参数。当调用 newCallable 时,newCallable 会调用 callable,并传给它 arg_list 中的参数。
arg_list 中的参数可能包含形如 _n 的名字,其中 n 是一个整数,这些参数是占位符,表示 newCallable 的参数,它们占据了传递给 newCallable 的参数的位置。数值 n 表示生成的可调用对象中参数的位置:_1 为 newCallable 的第一个参数,_2 为第二个参数,以此类推。_1 / _2 / _3… 这些占位符放在 placeholders 的一个命名空间中。
#include <iostream>
#include <iostream>
#include <functional>
using namespace std;using placeholders::_1;
using placeholders::_2;int add(int a, int b)
{return a + b;
}struct sub
{int operator()(int a, int b){return a - b;}
};int main()
{// bind 本身返回一个仿函数对象auto tadd = bind(add, _1, _2);auto tsub = bind(sub(), _1, _2);auto tmul = bind([](int a, int b) {return a * b; }, _1, _2);int a = 10;int b = 5;cout << tadd(a, b) << endl;cout << tsub(a, b) << endl;cout << tmul(a, b) << endl << endl;// bind 可以调整对象的参数顺序(但不常用)。// _1 代表第一个实参传入对象参数的位置// _2 代表第二个实参传入对象参数的位置// ... 其它同理auto radd = bind(add, _2, _1);auto rsub = bind(sub(), _2, _1);auto rmul = bind([](int a, int b) {return a * b; }, _2, _1);cout << radd(a, b) << endl;cout << rsub(a, b) << endl;cout << rmul(a, b) << endl << endl;// bind 调整对象参数个数(常用)。auto sub_10 = bind(sub(), _1, 10);auto _100_sub = bind(sub(), 100, _1);cout << sub_10(50) << endl;cout << _100_sub(-10) << endl;return 0;
}
function
std::function 是一个类模板,也是一个包装器。 std::function 的实例对象可以包装存储其他的可调用对象,包括函数指针、仿函数、 lambda 、 bind 表达式等,存储的可调用对象被称为 std::function 的目标。若 std::function 不含目标,则称它为空。调用空 std::function 的目标会导致抛出 std::bad_function_call 异常。
function 被定义 <functional> 头文件中。std::function - cppreference.com 是 std::function 的官方文件链接。
函数指针、仿函数、 lambda 等可调用对象的类型各不相同, std::function 的优势就是统一类型,对它们都可以进行包装,这样在很多地方就方便声明可调用对象的类型。
#include <iostream>
#include <functional>
using namespace std;int add(int a, int b)
{return a + b;
}struct sub
{int operator()(int a, int b){return a - b;}
};struct func
{static string getString(){return "得到字符串";}int getNumber(){return 10000;}
};int main()
{//function 的 <> 中参数放在 () 里,返回值在 () 的左边:function<string()> print = [] { return "hello world!"; };function<int(int, int)> tsub = sub();function<int(int, int)> tadd = add;cout << print() << endl;cout << tsub(10, 2) << endl;cout << tadd(10, 2) << endl;// 对于静态类成员函数function<string()> getstr1 = func::getString;function<string()> getstr2 = &func::getString;cout << getstr1() << endl;cout << getstr2() << endl;// 对于普通成员函数有麻烦的地方,因为有隐式的 this 指针,调用 function 时需要传入对象function<int(func&)> getnum1 = &func::getNumber;//function<int(func&)> getnum1 = func::getNumber; 普通成员函数需要取地址符号 '&'func one;cout << getnum1(one) << endl;// 解决方法:使用 bind 将对象的 this 指针进行绑定,方便调用function<int()> getnum2 = bind(&func::getNumber, func());cout << getnum2() << endl;return 0;
}
四、部分新内容添加
新的类功能
1.默认的移动构造和移动赋值
原来 C++ 类中,有 6 个默认成员函数:构造函数 / 析构函数 / 拷贝构造函数 / 拷贝赋值重载 / 取地址重载 / const 取地址重载,最后重要的是前 4 个,后两个用处不大,默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个默认成员函数,移动构造函数 和 移动赋值运算符重载。
如果没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。则编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
如果自己提供了移动构造或者移动赋值,编译器就不会自动提供拷贝构造和拷贝赋值。
2.声明时给缺省值
这部分内容可参考:(学习总结9)C++学习的初步总结 —— 三、缺省参数
3.defult 和 delete
C++11 可以更好的控制要使用的默认函数。假设要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。如果提供了拷贝构造,就不会生成移动构造,则可以使用 default 关键字显示指定移动构造生成。
如果能想要限制某些默认函数的生成,在 C++98 中,是该函数设置成 private,并且只声明,这样只要其他人想要调用就会报错。在 C++11 中更简单,只需在该函数声明加上 = delete 即可,该语法指示编译器不生成对应函数的默认版本,称 = delete 修饰的函数为删除函数。
#include <iostream>
using namespace std;class One
{
public:One() = default;~One() = default;One(const One&) = default;One& operator=(const One&) = default;One(One&&) = default;One& operator=(One&&) = default;
};class Two
{
public:Two() = delete;
};int main()
{One get1;//Two get2; // 默认构造已经删除,会报错return 0;
}
4.final 与 override
这部分内容可参考:
(学习总结17)C++继承 —— 四、派生类的默认成员函数 —— 实现一个不能被继承的类
(学习总结18)C++多态 —— 二、多态的定义及实现 —— 6. override 和 final 关键字
STL中一些变化
下图圈起来的就是 STL 中的新容器,但是实际上常用的是 unordered_map 和unordered_set。
STL 中容器的新接口也不少,最重要的就是右值引用和移动语义相关的 push / insert / emplace 系列接口 和 移动构造与移动赋值,还有 initializer_list 版本的构造等,容器的 范围for 遍历支持。