C++11实用特性2
1 可调用对象包装器、绑定器
1可调用对象
C++中的可调用对象分为四类:
- 函数指针:
任何一个函数都可以抽象成一个函数指针
int print(int a, double b)
{
cout << a << b << endl;
return 0;
}
// 定义函数指针
int (*func)(int, double) = &print;
- 仿函数(函数对象)
如果一个对象具有operator()成员函数,那么就称其为仿函数
#include <iostream>
#include <string>
#include <vector>
using namespace std;
struct Test
{
// ()操作符重载
void operator()(string msg)
{
cout << "msg: " << msg << endl;
}
};
int main(void)
{
Test t;
t("我是要成为海贼王的男人!!!"); // 仿函数
return 0;
}
- 类的静态函数
- 可转为函数指针的类对象(比较麻烦难懂,不推荐)
利用operator可将类对象转换为函数指针,需要在类内定义operator 函数指针()
,括号内只能为空,返回值要求是一个函数地址,且这个函数只能是属于类的静态成员函数;而此时就相当于调用这个对象并传入对应函数所需参数时,对应的函数就被调用
- 类成员函数指针、类成员变量指针(不好用)
定义非静态成员函数的函数指针时要加上类作用域才行,因为非静态成员函数在对象未分配前不存在地址
using fptr = void(Test::*) (int, string)//后面是参数列表,表明是属于Test类中的一个指针
2 可调用对象包装器function
function的出现使可调用对象变成一种统一的类型来使用,其使用语法如下:
#include <functional>
std::function<返回值类型(参数类型列表)> diy_name = 可调用对象;
要注意类成员函数指针是不能直接打包的,要借助bind才行
3 可调用对象绑定器
std::bind用来将可调用对象与其参数一起进行绑定。绑定后的结果可以使用std::function进行保存,并延迟调用到任何我们需要的时候。通俗来讲,它主要有两大作用:
- 将可调用对象与其参数一起绑定成一个
仿函数
。 - 将多元(参数个数为n,n>1)可调用对象转换为一元或者(n-1)元可调用对象,即只绑定部分参数。
其使用语法如下:
// 绑定非类成员函数/变量,静态成员函数
auto f = std::bind(可调用对象地址, 绑定的参数/占位符);
// 绑定类成员函/变量
auto f = std::bind(类函数/成员地址, 类实例对象地址, 绑定的参数/占位符);
指定第一个参数的时候加上类作用域
2 lambda表达式
1 基本语法
lambda表达式定义了一个匿名函数,并且可以捕获一定范围内的变量。其最大的好处就是使用时定义,lambda表达式的语法形式简单归纳如下:
[capture](params) opt -> ret {body;};
- capture:捕获列表,对函数体外部变量的捕获方式
[] - 不捕捉任何变量
[&] - 捕获外部作用域中所有变量, 并作为引用在函数体内使用 (按引用捕获)
[=] - 捕获外部作用域中所有变量, 并作为副本在函数体内使用 (按值捕获)
拷贝的副本在匿名函数体内部是只读的
[=, &foo] - 按值捕获外部作用域中所有变量, 并按照引用捕获外部变量 foo
[bar] - 按值捕获 bar 变量, 同时不捕获其他变量
[&bar] - 按引用捕获 bar 变量, 同时不捕获其他变量
[this] - 捕获当前类中的this指针
让lambda表达式拥有和当前类成员函数同样的访问权限
如果已经使用了 & 或者 =, 默认添加此选项
但是只能使用类中的成员变量
- params:参数列表,和普通函数参数列表一样,没有可以不写
- opt:函数选项,没有可以不写
- mutable: 可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)
- exception: 指定函数抛出的异常,如抛出整数类型的异常,可以使用throw();
- ret:返回值类型
- body:函数体
使用示例:
要注意匿名函数的大括号后面加上一个小括号才算调用
void func(int x, int y){
int a = 7;
int b = 9;
[=, &x](){
int c = a;
int d = x;
x++;
}();
cout<<x;
}
int main()
{
func(1,2);
return 0;
}
2 返回值
很多时候,lambda表达式的返回值是非常明显的,因此在C++11中允许省略lambda表达式的返回值。
// 完整的lambda表达式定义
auto f = [](int a) -> int
{
return a+10;
};
// 忽略返回值的lambda表达式定义
auto f = [](int a)
{
return a+10;
};
3 本质
lambda表达式的类型在C++11中会被看做是一个带operator()的类,即仿函数。
按照C++标准,lambda表达式的operator()默认是const的,一个const成员函数是无法修改成员变量值的。
mutable选项的作用就在于取消operator()的const属性。
由于lambda表达式在C++中会被看做是一个仿函数,因此可以使用std::function和std::bind来存储和操作lambda表达式:
#include <iostream>
#include <functional>
using namespace std;
int main(void)
{
// 包装可调用函数
std::function<int(int)> f1 = [](int a) {return a; };
// 绑定可调用函数
std::function<int(int)> f2 = bind([](int a) {return a; }, placeholders::_1);
// 函数调用
cout << f1(100) << endl;
cout << f2(200) << endl;
return 0;
}
3 右值引用
右值引用也是C++提供的新特性:
- 左值(location value)
指存储在内存中、有明确存储地址的数据,即可以取地址 - 右值(readable value)
可以提供数据值的数据,不可取地址,比如数字
首先明确,无论左值引用(&)还是右值引用(&&),它们的前提都是引用,就是个别名,而引用的实质就是个指针常量void* const
1 右值引用的作用
有时我们会通过一个临时对象来构造我们的对象,如果这个临时对象本身就很大,它创建出来已经耗费了一部分系统资源,然后还要再拷贝给我们自己的对象,拷贝完再销毁这个临时对象,整个过程的开销是很大的,我们就利用右值引用来延长这种临时对象的声明周期,使得可以直接使用它而不发生拷贝
2 移动构造函数
上面的问题就引出了类中的又一个构造函数--移动构造函数
,其就是为了解决这种不必要的拷贝,实现复用其它对象中的资源(堆内存),这本质上是一种浅拷贝,但区别在于移动构造中要让原对象指向这片地址的指针指向空,使得该地址的管理权被新的对象独享,或者理解为移动构造函数把临时对象的指针指向移走了
class Test
{
public:
Test(){
}
Test(Test&& a):m_num(a.m_num)
{
a.m_num = nullptr;
}
private:
int *m_num;
};
Test getRet(){
Test t;
return t;
}
int main()
{
//getRet在返回的时候会创建一个临时对象接受t,
Test t1 = getRet();
Test &&t2 = getRet();
return 0;
}
在添加了移动构造函数之后,对象赋值的时候编译器会判断等号右边的是否为临时对象,便选择哪一种构造函数来实现,如果没有移动构造函数,那么调用的就是拷贝构造函数了。
如今C++中的函数在返回一个类对象时,理论上我们会认为通过复制构造函数复制一个临时对象,然后在赋值给外部变量。实际上编译器做了优化,省去了中间的临时对象环节。所以有时我们无法看到移动构造被调用
3 &&
在C++中,并不是所有情况下 && 都代表是一个右值引用,具体的场景体现在模板和自动类型推导中,如果是模板参数需要指定为T&&
,如果是自动类型推导需要指定为auto &&
,在这两种场景下 &&被称作未定的引用类型,或者称之为万能引用。另外还有一点需要额外注意const T&&特指一个右值引用,不是未定引用类型。
template<typename T>
void f(T&& param);
void f1(const T&& param);
f(10);
int x = 10;
f(x);
f1(x); // error, x是左值
f1(10); // ok, 10是右值
由于上述代码中存在T&&或者auto&&这种未定引用类型,当它作为参数时,有可能被一个右值引用初始化,也有可能被一个左值引用初始化,在进行类型推导时右值引用类型(&&)会发生变化,这种变化被称为引用折叠。在C++11中引用折叠的规则如下:
-
通过右值推导 T&& 或者 auto&& 得到的是一个右值引用类型
-
通过非右值(右值引用、左值、左值引用、常量右值引用、常量左值引用)推导 T&& 或者 auto&& 得到的是一个左值引用类型
5 右值变左值
#include <iostream>
using namespace std;
void printValue(int &i)
{
cout << "l-value: " << i << endl;
}
void printValue(int &&i)
{
cout << "r-value: " << i << endl;
}
void forward(int &&k)
{
printValue(k);
}
int main()
{
int i = 520;
printValue(i);
printValue(1314);
forward(250);
return 0;
}
根据测试代码可以得知,编译器会根据传入的参数的类型(左值还是右值)调用对应的重置函数(printValue),函数forward()接收的是一个右值,但是在这个函数中调用函数printValue()时,参数k变成了一个命名对象,编译器会将其当做左值来处理。
4 转移和完美转发
1 转移move
在C++11添加了右值引用,并且不能使用左值初始化右值引用
,如果想要使用左值初始化一个右值引用需要借助std::move()函数,使用std::move方法可以将左值转换为右值
。使用这个函数并不能移动任何东西,而是和移动构造函数一样都具有移动语义,将对象的状态或者所有权从一个对象转移到另一个对象
,只是转移,没有内存拷贝,而这个左值被转移了控制权以后就变成了将亡值
语法:
template<class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) _NOEXCEPT
{ // forward _Arg as movable
return (static_cast<remove_reference_t<_Ty>&&>(_Arg));
}
由于一个右值在绑定过后再次使用时会被当作左值,所以需要用move来保持它的右值属性
int &&a = 10;
int &&b = move(a);//去掉move会报错,左值不能给右值赋值
使用场景:
- 用左值对右值初始化
- 一个对象不再使用,但是要拷贝它的数据到另一个对象中,用move来避免拷贝
2 forward
一个右值引用作为函数参数的形参时,在函数内部转发该参数给内部其他函数时,它就变成一个左值,并不是原来的类型了。forward用于保证引用的类型在传递过程中不会发生变化,其使用原型std::forward
- 当T为左值引用类型时,t将被转换为T类型的左值
- 当T不是左值引用类型时,t将被转换为T类型的右值
#include <iostream>
using namespace std;
template<typename T>
void printValue(T& t)
{
cout << "l-value: " << t << endl;
}
template<typename T>
void printValue(T&& t)
{
cout << "r-value: " << t << endl;
}
template<typename T>
void testForward(T && v)
{
printValue(v);
printValue(move(v));
printValue(forward<T>(v));
cout << endl;
}
int main()
{
testForward(520);
int num = 1314;
testForward(num);
testForward(forward<int>(num));
testForward(forward<int&>(num));
testForward(forward<int&&>(num));
return 0;
}