🔥个人主页: Forcible Bug Maker
🔥专栏: C++
目录
🌈前言
本篇博客主要内容:C++11中较为常用的新语法。
咱们继续接着上篇C++11来讲,本篇博客会涉及到C++11中新的两个默认成员函数,lambda表达式以及包装器相关的内容。
🔥类的新功能
新增默认成员函数
在原来所介绍的C++类中,有六个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const取地址重载
默认成员函数就是我们不写编译器会生成一个默认的。C++11中,又新增了两个,它们基于右值引用语法而出现,分别是:移动构造函数和移动赋值重载。
对于这两个函数,需要注意以下几点:
- 当你自己没有实现移动构造函数,析构函数,拷贝构造,拷贝复制重载中的任意一个时,那么编译器会自动生成一个默认移动构造。
默认生成的移动构造函数,对于内置类型的成员会按字节拷贝,对于自定义类型的成员,则需要看其是否实现了移动构造,如果实现了就调用移动构造,否则就调用其拷贝构造。 - 当你自己没有实现移动复制重载,析构函数,拷贝构造,拷贝赋值重载中的任意一个时,那么编译器会自动生成一个移动赋值重载。
默认生成的移动构造函数,对于内置类型的成员会按字节拷贝,对于自定义类型的成员,则需要看其是否实现了移动赋值重载,如果实现了就调用移动赋值重载,否则就调用其拷贝赋值。(移动赋值和上面的移动构造完全类似) - 如果你提供了移构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
强制生成默认函数的关键字default
C++11为了更好的控制要使用的默认函数,可以强制使一些因规则不必生成的默认函数生成。如:我们提供了拷贝构造,就不会生成移动构造了,那么我们这时就可以用default关键字显示指定移动构造生成。
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
Date(const Date& d)
:_year(d._year)
,_month(d._month)
,_day(d._day)
{}
// 强制生成
Date(Date&& d) = default;
private:
int _year;
int _month;
int _day;
};
禁止生成默认函数的关键字delete
如果想限制某些默认函数的生成,C++98提供的解决方案是,将该函数设置为private,并且只声明不实现,这样只要其他人想调用就会报错。在C++11中更简单,只需要在该函数声明后加上= delete
即可,该语法指示编译器不生成对应函数的默认版本,称delete修饰的函数为删除函数。
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
// 禁止拷贝构造
Date(const Date& d) = delete;
private:
int _year;
int _month;
int _day;
};
🔥可变参数模板
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。掌握一些基础的可变参数模板特性就够我们用了,所以这里点到为止,以后大家如果有需要,再可以深入学习。
下面是一个基本的可变参数模板:
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数
template<class ...Args>
void ShowList(Args... args)
{}
注:模板参数包名Args和函数形参参数包名args都是像变量一样可变的。
上面args前面有省略号,所以它是一个可变模板参数,我们把带省略号的参数称为“参数包”,它里面包含0~N(n>=0)个模板参数。我们无法直接取参数包args中的每个参数,只能通过展开参数包的方式来获取,这是使用参数包的一个主要特点,也是最大的难点,即如何展开可变参数。由于语法并不支持args[i]这样的用法,所以我们需要使用些特殊的方式来获取参数包的值。
递归函数方式展开参数包
// 递归终止函数
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;
}
通过将参数包逐层传递解析,最终可以遍历到参数包中的值。
逗号表达式展开参数包
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;
}
这种展开方式,不需要通过递归终止函数,直接在函数体中展开,同时运用了C++11的另一个特性——初始化列表,通过初始化一个变长数组,最终会创建一个元素值都为0的数值int arr[sizeof(Args)]
。expand函数中的逗号表达式:(printarg(args), 0),先执行printarg(args),再返回逗号表达式的结果0。由于是逗号表达式,在创建数组的过程中就将参数包展开了,这个数组的目的纯粹是我为了在构造的过程中展开参数包。
STL容器中的emplace相关接口函数
vector::emplace_back
list::emplace_back
template <class... Args>
void emplace_back (Args&&... args);
emplace系列的接口,支持模板的可变参数,并且是万能引用。那么emplace系列相对于传统的push_back和insert的优势体现在哪里呢?
std::list< std::pair<int, char> > mylist;
// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
mylist.emplace_back(10, 'a');
mylist.emplace_back(20, 'b');
mylist.emplace_back(make_pair(30, 'c'));
mylist.push_back(make_pair(40, 'd'));
mylist.push_back({ 50, 'e' });
for (auto e : mylist)
cout << e.first << ":" << e.second << endl;
其底层的区别是,emplace_back对于插入对象是直接构造;而push_back是先构造,再移动构造。相比之下emplace少了移动构造,效率会高些。
🔥lambda表达式
C++98中提供了sort算法,用于排序,每当排序结构体这种类型的元素时,就需要我们重新去写一个类。如果每次比较逻辑不同,还需要实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。
lambda表达式
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
// 价格升序
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._price < g2._price; });
//价格降序
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._price > g2._price; });
// 评估升序
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate < g2._evaluate; });
// 评估降序
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate > g2._evaluate; });
return 0;
}
上述代码就是使用C++的lambda表达式来解决,可以看出lambda表达式实际是一个匿名函数。
lambda表达式语法
lambda表达式书写格式:
[capture-list] (parameters) mutable ->return-type { statement}
表达式各部分说明:
- [capture-list]:捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda使用。
- (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。
- mutable:默认情况下,lambda函数总是一个const函数,添加mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- -> return-type:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注:在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 但该lambda函数不能做任何事情。
// 最简单的lambda表达式, 该lambda表达式没有任何意义
[] {};
// 省略参数列表和返回值类型,返回值类型由编译器推导为int
int a = 3, b = 4;
[=] {return a + 3; };
// 省略了返回值类型,无返回值类型
auto fun1 = [&](int c) {b = a + c; };
fun1(10);
cout << a << " " << b << endl << endl;
// 各部分都很完善的lambda函数
auto fun2 = [=, &b](int c)->int {return b += a + c; };
cout << fun2(10) << endl << endl;
// 复制捕捉x
int x = 10;
auto add_x = [x](int a) mutable { x *= 2; return a + x; };
cout << add_x(10) << endl;// 30
cout << x << endl;// 仍然是10
注:mutable使被捕获的变量可变其实比较迷惑,虽然可变,但其并不是传递过来x的引用,并不会影响lambda函数外部的x。
捕获列表说明:
捕捉列表描述了上下文中哪些数据可以被lambda使用,以及使用的方式为传值还是传引用。
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域和全局域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域和全局域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
⭐注:
- 父作用域指包含lambda函数的语句块
- 语法上捕捉列表可由多个捕捉项组成,以逗号分割
如:[=, &a, &b]:以传引用传递方式捕捉变量a和b,传值方式捕捉其他所有变量;[&, a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量。- 捕捉列表不允许变量重复传递,否则会导致编译错误
如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复- 在块作用域以外的lambda函数捕捉列表必须为空
- 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
- lambda表达式之间不能相互赋值,即使看起来类型相同
- 成员函数中的lambda对象,默认捕捉一个this指针
void (*PF)();
int main()
{
auto f1 = [] {cout << "hello world" << endl; };
auto f2 = [] {cout << "hello world" << endl; };
// 此处先不解释原因,等lambda表达式底层实现原理看完后,大家就清楚了
//f1 = f2; // 编译失败--->提示找不到operator=()
// 允许使用一个lambda表达式拷贝构造一个新的副本
auto f3(f2);
f3();
// 可以将lambda表达式赋值给相同类型的函数指针
PF = f2;
PF();
return 0;
}
函数对象和lambda表达式
函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的类对象。
class Rate
{
public:
Rate(double rate) : _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
int main()
{
// 函数对象
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
// lambda
auto r2 = [=](double monty, int year)->double {return monty * rate * year;
};
r2(10000, 2);
return 0;
}
从使用方式上来看,函数对象与lambda表达式完全一样。
函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。
如果我们转到底层去观察其汇编代码:
会发现,实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
🔥包装器
function
function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。
// std::function在头文件<functional>中
// 以下是模板原型
template <class T> function; // undefined
template <class Ret, class... Args> class function<Ret(Args...)>;
// 模板参数说明:
// Ret包装可调用对象的返回值类型
// Args...为可变参数模板(里面是被调用函数的形参)
关于function的具体使用如下:
#include <functional>
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator()(int a, int b)
{
return a + b;
}
};
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
// 函数名(函数指针)
std::function<int(int, int)> func1 = f;
cout << func1(1, 2) << endl;
// 函数对象
std::function<int(int, int)> func2 = Functor();
cout << func2(1, 2) << endl;
// lamber表达式
std::function<int(int, int)> func3 = [](const int a, const int b)
{return a + b; };
cout << func3(1, 2) << endl;
// 类的成员函数
// plusi为静态成员函数,取函数指针时可不加&符号
std::function<int(int, int)> func4 = &Plus::plusi;
cout << func4(1, 2) << endl;
// plusd为非静态成员函数,取函数指针时需要加&符号(规定)
// 由于的成员函数隐含this指针,所以需要多传一个参数
// std::function<double(Plus*, double, double)> func5 = &Plus::plusd;
// Plus pd;
// cout << func5(&pd, 1.1, 2.2) << endl;
// 以上三行和下面两行代码是同样效果
std::function<double(Plus, double, double)> func5 = &Plus::plusd;
cout << func5(Plus(), 1.1, 2.2) << endl;
// 传入Plus是转换调用,不需要完全和this匹配
return 0;
}
bind
std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。
总的来说,bind可以对可调用对象进行包装,同时支持调整参数的顺序和个数。
// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
调用bind的一般形式:
auto newCallable = 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 … _n为命名空间placeholders内的标识符。
使用举例:
#include <functional>
int Plus(int a, int b, int c)
{
return a + b + c;
}
class Sub
{
public:
int sub(int a, int b)
{
return a - b;
}
};
int main()
{
//表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定
std::function<int(int, int, int)> func1 = std::bind(Plus, placeholders::_1,
placeholders::_2, placeholders::_3);
//auto func1 = std::bind(Plus, placeholders::_1, placeholders::_2, placeholders::_3);
//表示绑定函数 plus 的第一,二为: 1, 2
auto func2 = std::bind(Plus, 1, 2, placeholders::_1);
cout << func1(1, 2, 3) << endl;
cout << func2(4) << endl;
Sub s;
// 绑定成员函数
std::function<int(int, int)> func3 = std::bind(&Sub::sub, s,
placeholders::_1, placeholders::_2);
// 参数调换顺序
std::function<int(int, int)> func4 = std::bind(&Sub::sub, s,
placeholders::_2, placeholders::_1);
cout << func3(1, 2) << endl;
cout << func4(1, 2) << endl;
return 0;
}
bind一般用于绑死一些固定参数。
bind的本质其实是返回一个仿函数对象。
🌈结语
本篇博客首先补充了一下C++11新增的两个默认成员函数,移动构造和移动赋值重载,可变参数模板以及STL容器中的emplace接口。然后讲解了lambda语法,以及包装器function和bind。lambda语法使程序员从每次调用相关算法都需要实现一个类的冗余工作中摆脱;包装器包装可调用对象的特性也使C++程序变得更加灵活。