C++篇(16)C++11(下)
一、可变参数模板
1.1 基本语法及原理
C++11支持可变参数模板,也就是支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包。存在两种参数包:模板参数包——表示零或多个模板参数;函数参数包——表示零或多个函数参数。

我们用省略号来指出一个模板参数或函数参数的表示一个包。在模板参数列表中,class... 或 typename... 指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟... 指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,和之前的普通模板一样,每个参数实例化时遵循引用折叠规则。
这里我们可以使用sizeof... 运算符去计算参数包中参数的个数。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;template<class ...Args>
void Print(Args&&... args)
{cout << sizeof...(args) << endl;
}int main()
{double x = 2.2;Print();Print(1);Print(1, string("xxxxxx"));Print(1, string("xxxxxx"), x);return 0;
}1.2 emplace系列接口
C++11以后STL容器新增了emplace系列的接口,emplace系列的接口均为模板可变参数,功能上兼容push和insert系列,但是emplace还支持其他功能。假设容器为container<T>,emplace还支持直接插入构造T对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造T对象。


传递参数包过程中,如果是Args&&... args的参数包,要用完美转发参数包,方式如下:std::forward<Args>(args)... ,否则编译时包扩展后右值引用变量表达式就变成了左值。
namespace bit
{template<class T>struct list_node{T _data;list_node<T>* _prev;list_node<T>* _next;list_node(const T& x):_data(x), _prev(nullptr), _next(nullptr){}list_node(T&& x = T()):_data(move(x)), _prev(nullptr), _next(nullptr){}template<class ...Args>list_node(Args&&... args):_data(forward<Args>(args)...), _prev(nullptr), _next(nullptr){ }};template<class T>class list{typedef list_node<T> Node;public:// ...template<class ...Args>void emplace_back(Args&&... args){emplace(end(), forward<Args>(args)...);}template<class ...Args>void emplace(iterator pos, T&& val){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(forward<Args>(args)...);newnode->_next = cur;newnode->_prev = prev;prev->_next = newnode;cur->_prev = newnode;++_size;}private:Node* _head;size_t _size = 0;};
}总体而言,综合各个场景,推荐使用emplace系列接口,综合效率更高。更推荐使用构造容器存储对象的参数包去插入,这样可以实现直接构造,效率更高。
二、新的类功能
2.1 默认的移动构造和移动赋值
在原来的C++类中,有6个默认成员函数:构造函数、析构函数、拷贝构造函数、拷贝赋值重载、取地址重载、const取地址重载。C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
如果没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会按字节拷贝,对于自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。(默认移动赋值和上面移动构造完全类似)
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;class Person
{
public:Person(const char* name = "",int age=0):_name(name),_age(age){ }/*Person(const Person& p):_name(p._name),_age(p._age){ }*//*Person& operator=(const Person& p){if (this != &p){_name = p._name;_age = p._age;}return *this;}*//*~Person(){ }*/private:bit::string _name;int _age;
};int main()
{Person(s1);Person s2 = s1;Person s3 = std::move(s1); //编译器为Person默认生成的移动构造Person s4;s4 = std::move(s2); //编译器为Person默认生成的移动赋值return 0;
}2.2 default和delete
C++11能帮助我们更好地控制要使用的默认函数。比如我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定生成移动构造。
如果想要限制某些默认函数的生成,在C++98中,是把该函数设置成私有,并且只声明不定义。而在C++11中更简单,只需要在函数声明加上 =delete 即可。该语法指示编译器不生成对应函数的默认版本,称 =delete修饰的函数为删除函数。
2.3 委托构造函数
C++中的委托构造函数(Delegating Constructor)是C++11引入的特性,允许一个构造函数调用同类中其他构造函数,从而减少代码重复并提高可维护性。被委托的构造函数必须初始化所有成员变量,因为委托构造函数后不能再重复初始化。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;class Example
{
public:Example(int a, int b):_x(a), _y(b){cout << "目标构造函数" << endl;}Example(int a):Example(a, 0){cout << "委托构造函数" << endl;}private:int _x;int _y;
};int main()
{Example(1, 2);Example(1);return 0;
}2.4 继承构造函数
继承构造函数是C++11引入的一项特性,它允许派生类直接继承基类的构造函数,不需要手动重新定义它们。这一特性简化了派生类的编写,特别是在基类有多个构造函数的情况下。派生类继承基类的普通构造函数,特殊的拷贝构造函数 / 移动构造函数不继承。
继承构造函数中派生类自己的成员变量如果有缺省值会使用缺省值初始化,如果没有缺省值就和之前类似——内置类型成员不确定,自定义类型成员调用默认构造初始化。
class Base
{
public:Base(int x, double d):_x(x),_d(d){ }Base(int x):_x(x){ }Base(double d):_x(d){ }protected:int _x = 0;double _d = 0;
};//传统的派生类实现构造
class Derived :public Base
{
public:Derived(int x):Base(x){ }Derived(double d):Base(d){ }Derived(int x, double d):Base(x, d){ }
};//C++11继承基类所有的构造函数
class Derived :public Base
{
public:using Base::Base;
};三、lambda表达式
3.1 lambda表达式语法
lambda表达式本质是一个匿名函数对象,和普通函数不同的是它可以定义在函数内部。lambda表达式语法使用层面而言没有类型,所以我们一般是用auto或者模板参数定义的对象去接收lambda对象。
lambda表达式的格式:[capture-list](parameters)-> return type{function body}
①[capture-list]:捕捉列表。编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文的变量供lambda函数来使用。捕捉列表可以传值和传引用捕捉,捕捉列表不能为空也不能省略。
②(parameters):参数列表。如果不需要传递参数,可以连同()一起省略。
③ -> return type:返回值类型。没有返回值时可以省略,一般返回值类型明确的情况下,也可以省略,由编译器对返回值类型进行推导。
④ {function body}:函数体。函数体为空也不能省略。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;int main()
{auto add1 = [](int x, int y)->int {return x + y;};cout << add1(1, 2) << endl;auto func1 = []{cout << "hello world" << endl;return 0;};func1();int a = 0, b = 1;auto swap1 = [](int& x, int& y){int tmp = x;x = y;y = tmp;};swap1(a, b);cout << a << ":" << b << endl;return 0;
}3.2 lambda的应用
在学习lambda表达式之前,我们使用的可调用对象只有函数指针和仿函数对象。函数指针类型定义起来比较麻烦,仿函数要定义一个类,相对会比较麻烦。使用lambda去定义可调用对象,既简单又方便。
lambda在很多地方也有用处,比如线程中的定义线程的执行函数逻辑,智能指针中定制删除器等等,lambda的应用很广泛,往后会不断接触到。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <vector>
#include <algorithm>struct Goods
{string _name;double _price;int _evaluate;// ...Goods(const char* str,double price,int evaluate):_name(str),_price(price),_evaluate(evaluate){ }
};struct ComparePriceLess
{bool operator()(const Goods& gl, const Goods& gr){return gl._price < gr._price;}
};struct ComparePriceGreater
{bool operator()(const Goods& gl, const Goods& gr){return gl._price > gr._price;}
};int main()
{vector<Goods> v = { {"苹果",2.1,5},{"香蕉",3,4},{"橙子",2.2,3},{"菠萝",1.5,4} };sort(v.begin(), v.end(), ComparePriceLess());sort(v.begin(), v.end(), ComparePriceGreater());auto priceLess = [](const Goods& gl, const Goods& gr){return gl._price < gr._price;};sort(v.begin(), v.end(), priceLess);sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr){return gl._price < gr._price;});sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr){return gl._price > gr._price;});sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr){return gl._evaluate > gr._evaluate;});return 0;
}3.3 捕捉列表
lambda表达式中默认用lambda函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉。
第一种捕捉方式是在捕捉列表中显示传值捕捉和传引用捕捉,捕捉的多个变量用逗号分隔。比如 [x, y, &z] 表示x和y值捕捉,z引用捕捉。
第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写一个=表示隐式值捕捉,在捕捉列表写一个&表示隐式引用捕捉。这样我们在lambda表达式中用了哪些变量,编译器就会自动捕捉那些变量。
第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[=,&x]表示其他变量隐式值捕捉,x引用捕捉;[&,x,y]表示其他变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第一个元素必须是&或=,并且&混合捕捉时,后面的捕捉变量必须是值捕捉。同理,=混合捕捉时,后面的捕捉变量必须是引用捕捉。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;int main()
{int a = 0, b = 1, c = 2, d = 3;auto func1 = [a, &b]{//值捕捉的变量不能修改,引用捕捉的变量可以修改//a++;b++;int ret = a + b;return ret;};cout << func1() << endl;auto func2 = [=]{int ret = a + b + c;return ret;};cout << func2() << endl;auto func3 = [&]{a++;c++;d++;};auto func4 = [&, a, b]{//a++;//b++;c++;d++;return a + b + c + d;};auto func5 = [=, &a, &b]{a++;b++;//c++;//d++;return a + b + c + d;};return 0;
}lambda表达式不能捕捉静态局部变量和全局变量(静态局部变量和全局变量也不需要捕捉, lambda表达式中可以直接使用)。如果lambda表达式如果定义在全局位置,捕捉列表必须为空。
默认情况下,lambda捕捉列表是被const修饰的,也就是说传值捕捉过来的对象不能修改。在参数列表后面加上mutable可以取消常量性,也就是使用该修饰符之后,传值捕捉的对象就可以修改了,但修改的还是形参对象,不会影响实参。使用该修饰符时,参数列表不可以省略(即使参数为空)
auto func7 = [=]() mutable
{a++;b++;c++;d++;
};四、包装器
4.1 function

std::function是一个类模板,也是一个包装器。std::function实例化的对象可以包装存储其他可调用对象,包括函数指针、仿函数、lambda、bind表达式等,存储的可调用对象被称为std::function的目标。若std::function不包含目标,则称它为空。调用空的std::function的目标会抛出std::bad_function_call的异常。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <vector>
#include <functional>
using namespace std;int f(int a, int b)
{return a + b;
}struct Functor
{
public:int operator()(int a, int b){return a + b;}
};int main()
{//包装各种可调用对象function<int(int, int)> f1 = f;function<int(int, int)> f2 = Functor();function<int(int, int)> f3 = [](int a, int b) {return a + b;};cout << f1(1, 2) << endl;cout << f2(1, 2) << endl;cout << f3(1, 2) << endl;vector<function<int(int, int)>> vf = { f,Functor(),[](int a, int b) {return a + b;} };for (auto& f : vf){cout << f(1, 2) << endl;}return 0;
}包装静态成员函数时,成员函数要指定类域并且在前面加&才能获取地址。
包装普通成员函数时,要注意普通成员函数还有一个隐藏的this指针参数,所以绑定时要传对象或者对象的指针过去。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <vector>
#include <functional>
using namespace std;class Plus
{
public:Plus(int n=10):_n(n){ }static int plusi(int a, int b){return a + b;}double plusd(double a, double b){return (a + b) * _n;}private:int _n;
};int main()
{function<int(int, int)> f4 = &Plus::plusi;cout << f4(1, 1) << endl;function<double(Plus*, double, double)> f5 = &Plus::plusd;Plus ps;cout << f5(&ps, 1.1, 1.1) << endl;return 0;
}当然我们还有下面两种方式:
function<double(Plus, double, double)> f6 = &Plus::plusd;
cout << f6(ps, 1.1, 1.1) << endl;
cout << f6(Plus(), 1.1, 1.1) << endl;function<double(Plus&&, double, double)> f7 = &Plus::plusd;
cout << f7(move(ps), 1.1, 1.1) << endl;
cout << f7(Plus(), 1.1, 1.1) << endl;有了function这个包装器,我们可以对之前写的逆波兰表达式求值的代码进行优化:
class Solution
{
public:int evalRPN(vector<string>& tokens){stack<int> st;map<string, function<int(int, int)>> opFuncMap = {{"+", [](int a, int b){return a + b;}},{"-", [](int a, int b){return a - b;}},{"*", [](int a, int b){return a * b;}},{"/", [](int a, int b){return a / b;}}};for(auto& str : tokens){if(opFuncMap.count(str)) //操作符{int right = st.top(); st.pop();int left = st.top(); st.pop();int ret = opFuncMap[str](left, right);st.push(ret);}else{st.push(stoi(str));}}return st.top();}
};4.2 bind

bind是一个函数模板,它也是一个可调用对象的包装器,可以把它看做是一个函数适配器。bind可以用来调整参数个数和参数顺序。
调用bind的一般形式:auto newCallable = bind(callable, arg_list)。其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是占位符。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个,以此类推。_1/_2/_3……这些占位符放在placeholders的命名空间中。
#include <iostream>
#include <functional>
using namespace std;using placeholders::_1;
using placeholders::_2;
using placeholders::_3;int Sub(int a, int b)
{return (a - b) * 10;
}int main()
{cout << Sub(10, 5) << endl; //50//bind本质返回一个仿函数对象//调整参数顺序(不常用)// _1表示第一个实参// _2表示第二个实参// ...auto newSub = bind(Sub, _2, _1);cout << newSub(10, 5) << endl; //-50return 0;
}
#include <iostream>
#include <functional>
using namespace std;using placeholders::_1;
using placeholders::_2;
using placeholders::_3;int Sub(int a, int b)
{return (a - b) * 10;
}int main()
{//调整参数个数auto newSub2 = bind(Sub, _1, 10);cout << newSub2(20) << endl; //100auto newSub3 = bind(Sub, 5, _1);cout << newSub3(20) << endl; //-150return 0;
}
#include <iostream>
#include <functional>
using namespace std;using placeholders::_1;
using placeholders::_2;
using placeholders::_3;class Plus
{
public:static int plusi(int a, int b){return a + b;}double plusd(double a, double b){return a + b;}
};int main()
{//成员函数对象进行绑死,就不需要每次传递了function<double(Plus&&, double, double)> f6 = &Plus::plusd;Plus ps;cout << f6(move(ps), 1.1, 1.1) << endl;cout << f6(Plus(), 1.1, 1.1) << endl;auto f7 = bind(&Plus::plusd, Plus(), _1, _2);cout << f7(1.1, 1.1) << endl;return 0;
}