网站建设岗位所需技能,网站设计机构图,wordpress 与 dede,建筑行业招聘网站推荐文章目录 C11新特性之auto和decltype知识点autoauto推导规则什么时候使用auto#xff1f; decltypedecltype推导规则 auto和decltype的配合使用 C11新特性之左值引用、右值引用、移动语义、完美转发左值、右值纯右值、将亡值纯右值将亡值左值引用、右值引用 移动语义深拷贝、浅… 文章目录 C11新特性之auto和decltype知识点autoauto推导规则什么时候使用auto decltypedecltype推导规则 auto和decltype的配合使用 C11新特性之左值引用、右值引用、移动语义、完美转发左值、右值纯右值、将亡值纯右值将亡值左值引用、右值引用 移动语义深拷贝、浅拷贝 完美转发返回值优化 C11新特性之列表初始化列表初始化的一些规则std::initializer_list列表初始化的好处 C11新特性std::function和lambda表达式std::functionstd::bindlambda表达式总结 C11新特性之模板改进模板的右尖括号模板的别名函数模板的默认模板参数 C11新特性之线程相关知识点std::thread相关std::mutex相关std::lock相关std::atomic相关std::call_once相关volatile相关std::condition_variable相关std::future相关std::promise与std::future配合使用std::packaged_task与std::future配合使用三者之间的关系 async相关总结 C11 的异步操作-asyncC11 使用 std::async创建异步程序std::futurestd::promisestd::packaged_taskstd::promise、std::packaged_task和std::future的关系为什么要用std::async代替线程的创建std::async基本用法总结 C11新特性之智能指针shared_ptrweak_ptrunique_ptr Lambda 表达式Lambda 表达式的基本语法如下lambda表达式的大致原理lambda表达式是不能被赋值的 C std::functionstd::function简介Member typesMember functions std::function使用 右值引用和move语义转移左值 C11新特性之auto和decltype知识点
C11引入了auto和decltype关键字使用它们可以在编译期就推导出变量或者表达式的类型方便开发者编码的同时也简化了代码。
auto
auto可以让编译器在编译器就推导出变量的类型看代码
auto a 10; // 10是int型可以自动推导出a是int
int i 10;auto b i; // b是int型
auto d 2.0; // d是double型这就是auto的基本用法可以通过右边的类型推导出变量的类型。
auto推导规则
直接看代码
代码1
int i 10;
auto a i, b i, *c i; // a是intb是i的引用c是i的指针auto就相当于int
auto d 0, f 1.0; // error0和1.0类型不同对于编译器有二义性没法推导
auto e; // error使用auto必须马上初始化否则无法推导类型代码2
void func(auto value) {} // errorauto不能用作函数参数class A {auto a 1; // error在类中auto不能用作非静态成员变量static auto b 1; // error这里与auto无关正常static int b 1也不可以static const auto int c 1; // ok
};void func2() {int a[10] {0};auto b a; // okauto c[10] a; // errorauto不能定义数组可以定义指针vectorint d;vectorauto f d; // errorauto无法推导出模板参数
}auto的限制 auto的使用必须马上初始化否则无法推导出类型 auto在一行定义多个变量时各个变量的推导不能产生二义性否则编译失败 auto不能用作函数参数 在类中auto不能用作非静态成员变量 auto不能定义数组可以定义指针 auto无法推导出模板参数
再看这段代码
int i 0;
auto *a i; // a是int*
auto b i; // b是int
auto c b; // c是int忽略了引用const auto d i; // d是const int
auto e d; // e是intconst auto f e; // f是const int
auto g f; // g是const int首先介绍下这里的cv是指const 和volatile
推导规则
在不声明为引用或指针时auto会忽略等号右边的引用类型和cv限定在声明为引用或者指针时auto会保留等号右边的引用和cv属性
什么时候使用auto
这里没有绝对答案在不影响代码代码可读性的前提下尽可能使用auto是蛮好的复杂类型就使用autoint、double这种就没有必要使用auto了看下面这段代码
auto func [] {cout xxx;
}; // 对于func难道不使用auto吗反正是不关心lambda表达式究竟是什么类型。auto asyncfunc std::async(std::launch::async, func);
// 对于asyncfunc难道不使用auto吗懒得写std::futurexxx等代码而且也记不住它返回的究竟是什么...decltype
上面介绍auto用于推导变量类型而decltype则用于推导表达式类型这里只用于编译器分析表达式的类型表达式实际不会进行运算上代码
int func() { return 0; }
decltype(func()) i; // i为int类型int x 0;
decltype(x) y; // y是int类型
decltype(x y) z; // z是int类型注意decltype不会像auto一样忽略引用和cv属性decltype会保留表达式的引用和cv属性
cont int i 1;
int a 2;
decltype(i) b 2; // b是const intdecltype推导规则
对于decltype(exp)有 exp是表达式decltype(exp)和exp类型相同 exp是函数调用decltype(exp)和函数返回值类型相同 其它情况若exp是左值decltype(exp)是exp类型的左值引用
int a 0, b 0;
decltype(a b) c 0; // c是int因为(ab)返回一个右值
decltype(a b) d c;// d是int因为(ab)返回一个左值d 20;
cout c c endl; // 输出c 20auto和decltype的配合使用
auto和decltype一般配合使用在推导函数返回值的类型问题上。
下面这段代码
templatetypename T, typename U
return_value add(T t, U u) { // t和v类型不确定无法推导出return_value类型return t u;
}上面代码由于t和u类型不确定那如何推导出返回值类型呢可能会想到这种
templatetypename T, typename U
decltype(t u) add(T t, U u) { // t和u尚未定义return t u;
}这段代码在C11上是编译不过的因为在decltype(t u)推导时t和u尚未定义就会编译出错所以有了下面的叫做返回类型后置的配合使用方法
templatetypename T, typename U
auto add(T t, U u) - decltype(t u) {return t u;
}返回值后置类型语法就是为了解决函数返回值类型依赖于参数但却难以确定返回值类型的问题。
C11新特性之左值引用、右值引用、移动语义、完美转发
C11新增了右值引用谈右值引用也可以扩展一些相关概念 左值 右值 纯右值 将亡值 左值引用 右值引用 移动语义 完美转发 返回值优化
左值、右值
概念1
左值可以放到等号左边的东西叫左值。
右值不可以放到等号左边的东西就叫右值。
概念2
左值可以取地址并且有名字的东西就是左值。
右值不能取地址的没有名字的东西就是右值。
举例
int a b c;a是左值有变量名可以取地址也可以放到等号左边, 表达式bc的返回值是右值没有名字且不能取地址(bc)不能通过编译而且也不能放到等号左边。
int a 4; // a是左值4作为普通字面量是右值左值一般有 函数名和变量名 返回左值引用的函数调用 前置自增自减表达式i、–i 由赋值表达式或赋值运算符连接的表达式(ab, a b等) 解引用表达式*p 字符串字面值abcd
纯右值、将亡值
纯右值和将亡值都属于右值。
纯右值
运算表达式产生的临时变量、不和对象关联的原始字面量、非引用返回的临时变量、lambda表达式等都是纯右值。
举例 除字符串字面值外的字面值 返回非引用类型的函数调用 后置自增自减表达式i、i– 算术表达式(ab, a*b, ab, ab等) 取地址表达式等(a)
将亡值
将亡值是指C11新增的和右值引用相关的表达式通常指将要被移动的对象、T函数的返回值、std::move函数的返回值、转换为T类型转换函数的返回值将亡值可以理解为即将要销毁的值通过“盗取”其它变量内存空间方式获取的值在确保其它变量不再被使用或者即将被销毁时可以避免内存空间的释放和分配延长变量值的生命周期常用来完成移动构造或者移动赋值的特殊任务。
举例
class A {xxx;
};
A a;
auto c std::move(a); // c是将亡值
auto d static_castA(a); // d是将亡值左值引用、右值引用
根据名字大概就可以猜到意思左值引用就是对左值进行引用的类型右值引用就是对右值进行引用的类型他们都是引用都是对象的一个别名并不拥有所绑定对象的堆存所以都必须立即初始化。
type name exp; // 左值引用
type name exp; // 右值引用左值引用
看代码
int a 5;
int b a; // b是左值引用
b 4;
int c 10; // error10无法取地址无法进行引用
const int d 10; // ok因为是常引用引用常量数字这个常量数字会存储在内存中可以取地址可以得出结论对于左值引用等号右边的值必须可以取地址如果不能取地址则会编译失败或者可以使用const引用形式但这样就只能通过引用来读取输出不能修改数组因为是常量引用。
右值引用
如果使用右值引用那表达式等号右边的值需要时右值可以使用std::move函数强制把左值转换为右值。
int a 4;
int b a; // error, a是左值
int c std::move(a); // ok移动语义
谈移动语义前首先需要了解深拷贝与浅拷贝的概念
深拷贝、浅拷贝
直接拿代码举例:
class A {
public:A(int size) : size_(size) {data_ new int[size];}A(){}A(const A a) {size_ a.size_;data_ a.data_;cout copy endl;}~A() {delete[] data_;}int *data_;int size_;
};
int main() {A a(10);A b a;cout b b.data_ endl;cout a a.data_ endl;return 0;
}上面代码中两个输出的是相同的地址a和b的data_指针指向了同一块内存这就是浅拷贝只是数据的简单赋值那再析构时data_内存会被释放两次导致程序出问题这里正常会出现double free导致程序崩溃的这样的程序肯定是有隐患的如何消除这种隐患呢可以使用如下深拷贝
class A {
public:A(int size) : size_(size) {data_ new int[size];}A(){}A(const A a) {size_ a.size_;data_ new int[size_];cout copy endl;}~A() {delete[] data_;}int *data_;int size_;
};
int main() {A a(10);A b a;cout b b.data_ endl;cout a a.data_ endl;return 0;
}深拷贝就是再拷贝对象时如果被拷贝对象内部还有指针引用指向其它资源自己需要重新开辟一块新内存存储资源而不是简单的赋值。
移动语义可以理解为转移所有权之前的拷贝是对于别人的资源自己重新分配一块内存存储复制过来的资源而对于移动语义类似于转让或者资源窃取的意思对于那块资源转为自己所拥有别人不再拥有也不会再使用通过C11新增的移动语义可以省去很多拷贝负担怎么利用移动语义呢是通过移动构造函数。
class A {
public:A(int size) : size_(size) {data_ new int[size];}A(){}A(const A a) {size_ a.size_;data_ new int[size_];cout copy endl;}A(A a) {this-data_ a.data_;a.data_ nullptr;cout move endl;}~A() {if (data_ ! nullptr) {delete[] data_;}}int *data_;int size_;
};
int main() {A a(10);A b a;A c std::move(a); // 调用移动构造函数return 0;
}如果不使用std::move()会有很大的拷贝代价使用移动语义可以避免很多无用的拷贝提供程序性能C所有的STL都实现了移动语义方便使用。例如
std::vectorstring vecs;
...
std::vectorstring vecm std::move(vecs); // 免去很多拷贝注意移动语义仅针对于那些实现了移动构造函数的类的对象对于那种基本类型int、float等没有任何优化作用还是会拷贝因为它们实现没有对应的移动构造函数。
完美转发
完美转发指可以写一个接受任意实参的函数模板并转发到其它函数目标函数会收到与转发函数完全相同的实参转发函数实参是左值那目标函数实参也是左值转发函数实参是右值那目标函数实参也是右值。那如何实现完美转发呢答案是使用std::forward()。
void PrintV(int t) {cout lvalue endl;
}void PrintV(int t) {cout rvalue endl;
}templatetypename T
void Test(T t) {PrintV(t);PrintV(std::forwardT(t));PrintV(std::move(t));
}int main() {Test(1); // lvalue rvalue rvalueint a 1;Test(a); // lvalue lvalue rvalueTest(std::forwardint(a)); // lvalue rvalue rvalueTest(std::forwardint(a)); // lvalue lvalue rvalueTest(std::forwardint(a)); // lvalue rvalue rvaluereturn 0;
}分析 Test(1)1是右值模板中T t这种为万能引用右值1传到Test函数中变成了右值引用但是调用PrintV()时候t变成了左值因为它变成了一个拥有名字的变量所以打印lvalue而PrintV(std::forward(t))时候会进行完美转发按照原来的类型转发所以打印rvaluePrintV(std::move(t))毫无疑问会打印rvalue。 Test(a)a是左值模板中T 这种为万能引用左值a传到Test函数中变成了左值引用所以有代码中打印。 Test(std::forward(a))转发为左值还是右值依赖于TT是左值那就转发为左值T是右值那就转发为右值。
返回值优化
返回值优化(RVO)是一种C编译优化技术当函数需要返回一个对象实例时候就会创建一个临时对象并通过复制构造函数将目标对象复制到临时对象这里有复制构造函数和析构函数会被多余的调用到有代价而通过返回值优化C标准允许省略调用这些复制构造函数。
那什么时候编译器会进行返回值优化呢?
return的值类型与函数的返回值类型相同return的是一个局部对象
看几个例子:
示例1
std::vectorint return_vector(void) {std::vectorint tmp {1,2,3,4,5};return tmp;
}
std::vectorint rval_ref return_vector();不会触发RVO拷贝构造了一个临时的对象临时对象的生命周期和rval_ref绑定等价于下面这段代码
const std::vectorint rval_ref return_vector();示例2
std::vectorint return_vector(void) {std::vectorint tmp {1,2,3,4,5};return std::move(tmp);
}std::vectorint rval_ref return_vector();这段代码会造成运行时错误因为rval_ref引用了被析构的tmp。讲道理来说这段代码是错的自己运行过程中却成功了继续向下看什么时候会触发RVO。
示例3
std::vectorint return_vector(void) {std::vectorint tmp {1,2,3,4,5};return std::move(tmp);
}std::vectorint rval_ref return_vector();和示例1类似std::move一个临时对象是没有必要的也会忽略掉返回值优化。
最好的代码
std::vectorint return_vector(void) {std::vectorint tmp {1,2,3,4,5};return tmp;
}std::vectorint rval_ref return_vector();这段代码会触发RVO不拷贝也不移动不生成临时对象。
C11新特性之列表初始化
C11新增了列表初始化的概念。
在C11中可以直接在变量名后面加上初始化列表来进行对象的初始化。
struct A {public:A(int) {}private:A(const A) {}
};
int main() {A a(123);A b 123; // errorA c { 123 };A d{123}; // c11int e {123};int f{123}; // c11return 0;
}列表初始化也可以用在函数的返回值上
std::vectorint func() {return {};
}列表初始化的一些规则
首先说下聚合类型可以进行直接列表初始化这里需要了解什么是聚合类型
类型是一个普通数组如int[5]char[]double[]等类型是一个类且满足以下条件 没有用户声明的构造函数没有用户提供的构造函数(允许显示预置或弃置的构造函数) 没有私有或保护的非静态数据成员没有基类 没有虚函数没有{}和直接初始化的非静态数据成员 没有默认成员初始化器
struct A {int a;int b;int c;A(int, int){}
};
int main() {A a{1, 2, 3};// errorA有自定义的构造函数不能列表初始化
}上述代码类A不是聚合类型无法进行列表初始化必须以自定义的构造函数来构造对象。
struct A {int a;int b;virtual void func() {} // 含有虚函数不是聚合类
};struct Base {};
struct B : public Base { // 有基类不是聚合类int a;int b;
};struct C {int a;int b 10; // 有等号初始化不是聚合类
};struct D {int a;int b;private:int c; // 含有私有的非静态数据成员不是聚合类
};struct E {int a;int b;E() : a(0), b(0) {} // 含有默认成员初始化器不是聚合类
};上面列举了一些不是聚合类的例子对于一个聚合类型使用列表初始化相当于对其中的每个元素分别赋值对于非聚合类型需要先自定义一个对应的构造函数此时列表初始化将调用相应的构造函数。
std::initializer_list
平时开发使用STL过程中可能发现它的初始化列表可以是任意长度大家有没有想过它是怎么实现的呢答案是std::initializer_list看下面这段示例代码
struct CustomVec {std::vectorint data;CustomVec(std::initializer_listint list) {for (auto iter list.begin(); iter ! list.end(); iter) {data.push_back(*iter);}}
};这个std::initializer_list其实也可以作为函数参数。
注意std::initializer_list它可以接收任意长度的初始化列表但是里面必须是相同类型T或者都可以转换为T。
列表初始化的好处
列表初始化的好处如下 方便且基本上可以替代括号初始化 可以使用初始化列表接受任意长度 可以防止类型窄化避免精度丢失的隐式类型转换
什么是类型窄化列表初始化通过禁止下列转换对隐式转化加以限制 从浮点类型到整数类型的转换 从 long double 到 double 或 float 的转换以及从 double 到 float 的转换除非源是常量表达式且不发生溢出 从整数类型到浮点类型的转换除非源是其值能完全存储于目标类型的常量表达式 从整数或无作用域枚举类型到不能表示原类型所有值的整数类型的转换除非源是其值能完全存储于目标类型的常量表达式
示例
int main() {int a 1.2; // okint b {1.2}; // errorfloat c 1e70; // okfloat d {1e70}; // errorfloat e (unsigned long long)-1; // okfloat f {(unsigned long long)-1}; // errorfloat g (unsigned long long)1; // okfloat h {(unsigned long long)1}; // okconst int i 1000;const int j 2;char k i; // okchar l {i}; // errorchar m j; // okchar m {j}; // ok因为是const类型这里如果去掉const属性也会报错
}打印如下
test.cc:24:17: error: narrowing conversion of ‘1.2e0’ from ‘double’ to ‘int’ inside { } [-Wnarrowing]int b {1.2};^
test.cc:27:20: error: narrowing conversion of ‘1.0000000000000001e70’ from ‘double’ to ‘float’ inside { } [-Wnarrowing]float d {1e70};test.cc:30:38: error: narrowing conversion of ‘18446744073709551615’ from ‘long long unsigned int’ to ‘float’ inside { } [-Wnarrowing]float f {(unsigned long long)-1};^
test.cc:36:14: warning: overflow in implicit constant conversion [-Woverflow]char k i;^
test.cc:37:16: error: narrowing conversion of ‘1000’ from ‘int’ to ‘char’ inside { } [-Wnarrowing]char l {i};C11新特性std::function和lambda表达式
c11新增了std::function、std::bind、lambda表达式等封装使函数调用更加方便。
std::function
讲std::function前首先需要了解下什么是可调用对象
满足以下条件之一就可称为可调用对象 是一个函数指针 是一个具有operator()成员函数的类对象(传说中的仿函数)lambda表达式 是一个可被转换为函数指针的类对象 是一个类成员(函数)指针 bind表达式或其它函数对象
而std::function就是上面这种可调用对象的封装器可以把std::function看做一个函数对象用于表示函数这个抽象概念。std::function的实例可以存储、复制和调用任何可调用对象存储的可调用对象称为std::function的目标若std::function不含目标则称它为空调用空的std::function的目标会抛出std::bad_function_call异常。
使用参考如下实例代码
std::functionvoid(int) f; // 这里表示function的对象f的参数是int返回值是void
#include functional
#include iostreamstruct Foo {Foo(int num) : num_(num) {}void print_add(int i) const { std::cout num_ i \n; }int num_;
};void print_num(int i) { std::cout i \n; }struct PrintNum {void operator()(int i) const { std::cout i \n; }
};int main() {// 存储自由函数std::functionvoid(int) f_display print_num;f_display(-9);// 存储 lambdastd::functionvoid() f_display_42 []() { print_num(42); };f_display_42();// 存储到 std::bind 调用的结果std::functionvoid() f_display_31337 std::bind(print_num, 31337);f_display_31337();// 存储到成员函数的调用std::functionvoid(const Foo, int) f_add_display Foo::print_add;const Foo foo(314159);f_add_display(foo, 1);f_add_display(314159, 1);// 存储到数据成员访问器的调用std::functionint(Foo const) f_num Foo::num_;std::cout num_: f_num(foo) \n;// 存储到成员函数及对象的调用using std::placeholders::_1;std::functionvoid(int) f_add_display2 std::bind(Foo::print_add, foo, _1);f_add_display2(2);// 存储到成员函数和对象指针的调用std::functionvoid(int) f_add_display3 std::bind(Foo::print_add, foo, _1);f_add_display3(3);// 存储到函数对象的调用std::functionvoid(int) f_display_obj PrintNum();f_display_obj(18);
}从上面可以看到std::function的使用方法当给std::function填入合适的参数表和返回值后它就变成了可以容纳所有这一类调用方式的函数封装器。std::function还可以用作回调函数或者在C里如果需要使用回调那就一定要使用std::function特别方便。
std::bind
使用std::bind可以将可调用对象和参数一起绑定绑定后的结果使用std::function进行保存并延迟调用到任何需要的时候。
std::bind通常有两大作用
将可调用对象与参数一起绑定为另一个std::function供调用将n元可调用对象转成m(m n)元可调用对象绑定一部分参数这里需要使用std::placeholders
具体示例
#include functional
#include iostream
#include memoryvoid f(int n1, int n2, int n3, const int n4, int n5) {std::cout n1 n2 n3 n4 n5 std::endl;
}int g(int n1) { return n1; }struct Foo {void print_sum(int n1, int n2) { std::cout n1 n2 std::endl; }int data 10;
};int main() {using namespace std::placeholders; // 针对 _1, _2, _3...// 演示参数重排序和按引用传递int n 7;// _1 与 _2 来自 std::placeholders 并表示将来会传递给 f1 的参数auto f1 std::bind(f, _2, 42, _1, std::cref(n), n);n 10;f1(1, 2, 1001); // 1 为 _1 所绑定 2 为 _2 所绑定不使用 1001// 进行到 f(2, 42, 1, n, 7) 的调用// 嵌套 bind 子表达式共享占位符auto f2 std::bind(f, _3, std::bind(g, _3), _3, 4, 5);f2(10, 11, 12); // 进行到 f(12, g(12), 12, 4, 5); 的调用// 绑定指向成员函数指针Foo foo;auto f3 std::bind(Foo::print_sum, foo, 95, _1);f3(5);// 绑定指向数据成员指针auto f4 std::bind(Foo::data, _1);std::cout f4(foo) std::endl;// 智能指针亦能用于调用被引用对象的成员std::cout f4(std::make_sharedFoo(foo)) std::endl;
}lambda表达式
lambda表达式可以说是c11引用的最重要的特性之一它定义了一个匿名函数可以捕获一定范围的变量在函数内部使用一般有如下语法形式
auto func [capture] (params) opt - ret { func_body; };其中func是可以当作lambda表达式的名字作为一个函数使用capture是捕获列表params是参数表opt是函数选项(mutable之类) ret是返回值类型func_body是函数体。
一个完整的lambda表达式
auto func1 [](int a) - int { return a 1; };
auto func2 [](int a) { return a 2; };
cout func1(1) func2(2) endl;如上代码很多时候lambda表达式返回值是很明显的c11允许省略表达式的返回值定义。
lambda表达式允许捕获一定范围内的变量 []不捕获任何变量 []引用捕获捕获外部作用域所有变量在函数体内当作引用使用 []值捕获捕获外部作用域所有变量在函数内内有个副本使用 [, a]值捕获外部作用域所有变量按引用捕获a变量 [a]只值捕获a变量不捕获其它变量 [this]捕获当前类中的this指针
lambda表达式示例代码
int a 0;
auto f1 [](){ return a; }; // 值捕获a
cout f1() endl;auto f2 []() { return a; }; // 修改按值捕获的外部变量error
auto f3 []() mutable { return a; };代码中的f2是编译不过的因为修改了按值捕获的外部变量其实lambda表达式就相当于是一个仿函数仿函数是一个有operator()成员函数的类对象这个operator()默认是const的所以不能修改成员变量而加了mutable就是去掉const属性。
还可以使用lambda表达式自定义stl的规则例如自定义sort排序规则
struct A {int a;int b;
};int main() {vectorA vec;std::sort(vec.begin(), vec.end(), [](const A left, const A right) { return left.a right.a; });
}总结
std::function和std::bind在平时编程过程中封装函数更加的方便而lambda表达式将这种方便发挥到了极致可以在需要的时间就地定义匿名函数不再需要定义类或者函数等在自定义STL规则时候也非常方便让代码更简洁更灵活提高开发效率。
C11新特性之模板改进
C11关于模板有一些细节的改进 模板的右尖括号 模板的别名 函数模板的默认模板参数
模板的右尖括号
C11之前是不允许两个右尖括号出现的会被认为是右移操作符所以需要中间加个空格进行分割避免发生编译错误。
模板的别名
C11引入了using可以轻松的定义别名而不是使用繁琐的typedef。
int main() {std::vectorstd::vectorint a; // errorstd::vectorstd::vectorint b; // ok
}使用using明显简洁并且易读大家可能之前也见过使用typedef定义函数指针之类的操作。
typedef void (*func)(int, int);
using func void (*)(int, int); // 起码比typedef容易看的懂上面的代码使用using起码比typedef容易看的懂一些但是我还是看不懂因为我从来不用这种来表示函数指针用std::function()、std::bind()、std::placeholder()、lambda表达式它不香吗。
函数模板的默认模板参数
C11之前只有类模板支持默认模板参数函数模板是不支持默认模板参数的C11后都支持。
template typename T, typename Uint
class A {T value;
};template typename Tint, typename U // error
class A {T value;
};类模板的默认模板参数必须从右往左定义而函数模板则没有这个限制。
template typename R, typename Uint
R func1(U val) {return val;
}template typename Rint, typename U
R func2(U val) {return val;
}int main() {cout func1int, double(99.9) endl; // 99cout func1double, double(99.9) endl; // 99.9cout func1double(99.9) endl; // 99.9cout func1int(99.9) endl; // 99cout func2int, double(99.9) endl; // 99cout func1double, double(99.9) endl; // 99.9cout func2double(99.9) endl; // 99.9cout func2int(99.9) endl; // 99return 0;
}C11新特性之线程相关知识点
c11关于并发引入了好多新东西这里按照如下顺序介绍 std::thread相关 std::mutex相关 std::lock相关 std::atomic相关 std::call_once相关 volatile相关 std::condition_variable相关 std::future相关 async相关
std::thread相关
c11之前可能使用pthread_xxx来创建线程繁琐且不易读c11引入了std::thread来创建线程支持对线程join或者detach。直接看代码
#include iostream
#include threadusing namespace std;int main() {auto func []() {for (int i 0; i 10; i) {cout i ;}cout endl;};std::thread t(func);if (t.joinable()) {t.detach();}auto func1 [](int k) {for (int i 0; i k; i) {cout i ;}cout endl;};std::thread tt(func1, 20);if (tt.joinable()) { // 检查线程可否被jointt.join();}return 0;
}上述代码中函数func和func1运行在线程对象t和tt中从刚创建对象开始就会新建一个线程用于执行函数调用join函数将会阻塞主线程直到线程函数执行结束线程函数的返回值将会被忽略。如果不希望线程被阻塞执行可以调用线程对象的detach函数表示将线程和线程对象分离。
如果没有调用join或者detach函数假如线程函数执行时间较长此时线程对象的生命周期结束调用析构函数清理资源这时可能会发生错误这里有两种解决办法一个是调用join()保证线程函数的生命周期和线程对象的生命周期相同另一个是调用detach()将线程和线程对象分离这里需要注意如果线程已经和对象分离那就再也无法控制线程什么时候结束了不能再通过join来等待线程执行完。
这里可以对thread进行封装避免没有调用join或者detach可导致程序出错的情况出现
class ThreadGuard {public:enum class DesAction { join, detach };ThreadGuard(std::thread t, DesAction a) : t_(std::move(t)), action_(a){};~ThreadGuard() {if (t_.joinable()) {if (action_ DesAction::join) {t_.join();} else {t_.detach();}}}ThreadGuard(ThreadGuard) default;ThreadGuard operator(ThreadGuard) default;std::thread get() { return t_; }private:std::thread t_;DesAction action_;
};int main() {ThreadGuard t(std::thread([]() {for (int i 0; i 10; i) {std::cout thread guard i ;}std::cout std::endl;}), ThreadGuard::DesAction::join);return 0;
}c11还提供了获取线程id或者系统cpu个数获取thread native_handle使得线程休眠等功能
std::thread t(func);
cout 当前线程ID t.get_id() endl;
cout 当前cpu个数 std::thread::hardware_concurrency() endl;
auto handle t.native_handle();// handle可用于pthread相关操作
std::this_thread::sleep_for(std::chrono::seconds(1));std::mutex相关
std::mutex是一种线程同步的手段用于保存多线程同时操作的共享数据。
mutex分为四种 std::mutex独占的互斥量不能递归使用不带超时功能 std::recursive_mutex递归互斥量可重入不带超时功能 std::timed_mutex带超时的互斥量不能递归 std::recursive_timed_mutex带超时的互斥量可以递归使用
拿一个std::mutex和std::timed_mutex举例别的都是类似的使用方式
std::mutex:
#include iostream
#include mutex
#include threadusing namespace std;
std::mutex mutex_;int main() {auto func1 [](int k) {mutex_.lock();for (int i 0; i k; i) {cout i ;}cout endl;mutex_.unlock();};std::thread threads[5];for (int i 0; i 5; i) {threads[i] std::thread(func1, 200);}for (auto th : threads) {th.join();}return 0;
}std::timed_mutex:
#include iostream
#include mutex
#include thread
#include chronousing namespace std;
std::timed_mutex timed_mutex_;int main() {auto func1 [](int k) {timed_mutex_.try_lock_for(std::chrono::milliseconds(200));for (int i 0; i k; i) {cout i ;}cout endl;timed_mutex_.unlock();};std::thread threads[5];for (int i 0; i 5; i) {threads[i] std::thread(func1, 200);}for (auto th : threads) {th.join();}return 0;
}std::lock相关
这里主要介绍两种RAII方式的锁封装可以动态的释放锁资源防止线程由于编码失误导致一直持有锁。
c11主要有std::lock_guard和std::unique_lock两种方式使用方式都类似如下
#include iostream
#include mutex
#include thread
#include chronousing namespace std;
std::mutex mutex_;int main() {auto func1 [](int k) {// std::lock_guardstd::mutex lock(mutex_);std::unique_lockstd::mutex lock(mutex_);for (int i 0; i k; i) {cout i ;}cout endl;};std::thread threads[5];for (int i 0; i 5; i) {threads[i] std::thread(func1, 200);}for (auto th : threads) {th.join();}return 0;
}std::lock_gurad相比于std::unique_lock更加轻量级少了一些成员函数std::unique_lock类有unlock函数可以手动释放锁所以条件变量都配合std::unique_lock使用而不是std::lock_guard因为条件变量在wait时需要有手动释放锁的能力具体关于条件变量后面会讲到。
std::atomic相关
c11提供了原子类型std::atomic理论上这个T可以是任意类型但是平时只存放整形别的还真的没用过整形有这种原子变量已经足够方便就不需要使用std::mutex来保护该变量啦。看一个计数器的代码
struct OriginCounter { // 普通的计数器int count;std::mutex mutex_;void add() {std::lock_guardstd::mutex lock(mutex_);count;}void sub() {std::lock_guardstd::mutex lock(mutex_);--count;}int get() {std::lock_guardstd::mutex lock(mutex_);return count;}
};struct NewCounter { // 使用原子变量的计数器std::atomicint count;void add() {count;// count.store(count);这种方式也可以}void sub() {--count;// count.store(--count);}int get() {return count.load();}
};是不是使用原子变量更加方便了呢
std::call_once相关
c11提供了std::call_once来保证某一函数在多线程环境中只调用一次它需要配合std::once_flag使用直接看使用代码
std::once_flag onceflag;void CallOnce() {std::call_once(onceflag, []() {cout call once endl;});
}int main() {std::thread threads[5];for (int i 0; i 5; i) {threads[i] std::thread(CallOnce);}for (auto th : threads) {th.join();}return 0;
}volatile相关
貌似把volatile放在并发里介绍不太合适但是貌似很多人都会把volatile和多线程联系在一起一起介绍下。
volatile通常用来建立内存屏障volatile修饰的变量编译器对访问该变量的代码通常不再进行优化看下面代码
int *p xxx;
int a *p;
int b *p;a和b都等于p指向的值一般编译器会对此做优化把*p的值放入寄存器就是传说中的工作内存(不是主内存)之后a和b都等于寄存器的值但是如果中间p地址的值改变内存上的值改变啦但a,b还是从寄存器中取的值(不一定看编译器优化结果)这就不符合需求所以在此对p加volatile修饰可以避免进行此类优化。
注意volatile不能解决多线程安全问题针对特种内存才需要使用volatile它和atomic的特点如下• std::atomic用于多线程访问的数据且不用互斥量用于并发编程中• volatile用于读写操作不可以被优化掉的内存用于特种内存中
std::condition_variable相关
条件变量是c11引入的一种同步机制它可以阻塞一个线程或者个线程直到有线程通知或者超时才会唤醒正在阻塞的线程条件变量需要和锁配合使用这里的锁就是上面介绍的std::unique_lock。
这里使用条件变量实现一个CountDownLatch
class CountDownLatch {public:explicit CountDownLatch(uint32_t count) : count_(count);void CountDown() {std::unique_lockstd::mutex lock(mutex_);--count_;if (count_ 0) {cv_.notify_all();}}void Await(uint32_t time_ms 0) {std::unique_lockstd::mutex lock(mutex_);while (count_ 0) {if (time_ms 0) {cv_.wait_for(lock, std::chrono::milliseconds(time_ms));} else {cv_.wait(lock);}}}uint32_t GetCount() const {std::unique_lockstd::mutex lock(mutex_);return count_;}private:std::condition_variable cv_;mutable std::mutex mutex_;uint32_t count_ 0;
};关于条件变量其实还涉及到通知丢失和虚假唤醒问题因为不是本文的主题这里暂不介绍大家有需要可以留言。
std::future相关
c11关于异步操作提供了future相关的类主要有std::future、std::promise和std::packaged_taskstd::future比std::thread高级些std::future作为异步结果的传输通道通过get()可以很方便的获取线程函数的返回值std::promise用来包装一个值将数据和future绑定起来而std::packaged_task则用来包装一个调用对象将函数和future绑定起来方便异步调用。而std::future是不可以复制的如果需要复制放到容器中可以使用std::shared_future。
std::promise与std::future配合使用
#include functional
#include future
#include iostream
#include threadusing namespace std;void func(std::futureint fut) {int x fut.get();cout value: x endl;
}int main() {std::promiseint prom;std::futureint fut prom.get_future();std::thread t(func, std::ref(fut));prom.set_value(144);t.join();return 0;
}std::packaged_task与std::future配合使用
#include functional
#include future
#include iostream
#include threadusing namespace std;int func(int in) {return in 1;
}int main() {std::packaged_taskint(int) task(func);std::futureint fut task.get_future();std::thread(std::move(task), 5).detach();cout result fut.get() endl;return 0;
}三者之间的关系
std::future用于访问异步操作的结果而std::promise和std::packaged_task在future高一层它们内部都有一个futurepromise包装的是一个值packaged_task包装的是一个函数当需要获取线程中的某个值可以使用std::promise当需要获取线程函数返回值可以使用std::packaged_task。
async相关
async是比futurepackaged_taskpromise更高级的东西它是基于任务的异步操作通过async可以直接创建异步的任务返回的结果会保存在future中不需要像packaged_task和promise那么麻烦关于线程操作应该优先使用async看一段使用代码
#include functional
#include future
#include iostream
#include threadusing namespace std;int func(int in) { return in 1; }int main() {auto res std::async(func, 5);// res.wait();cout res.get() endl; // 阻塞直到函数返回return 0;
}使用async异步执行函数是不是方便多啦。
async具体语法如下
async(std::launch::async | std::launch::deferred, func, args...);第一个参数是创建策略
std::launch::async表示任务执行在另一线程std::launch::deferred表示延迟执行任务调用get或者wait时才会执行不会创建线程惰性执行在当前线程。
如果不明确指定创建策略以上两个都不是async的默认策略而是未定义它是一个基于任务的程序设计内部有一个调度器(线程池)会根据实际情况决定采用哪种策略。
若从 std::async 获得的 std::future 未被移动或绑定到引用则在完整表达式结尾 std::future的析构函数将阻塞直至异步计算完成实际上相当于同步操作
std::async(std::launch::async, []{ f(); }); // 临时量的析构函数等待 f()
std::async(std::launch::async, []{ g(); }); // f() 完成前不开始注意关于async启动策略这里以cppreference为主。
有时候如果想真正执行异步操作可以对async进行封装强制使用std::launch::async策略来调用async。
template typename F, typename... Args
inline auto ReallyAsync(F f, Args... params) {return std::async(std::launch::async, std::forwardF(f), std::forwardArgs(params)...);
}总结
• std::thread使线程的创建变得非常简单还可以获取线程id等信息。 • std::mutex通过多种方式保证了线程安全互斥量可以独占也可以重入还可以设置互斥量的超时时间避免一直阻塞等锁。 • std::lock通过RAII技术方便了加锁和解锁调用有std::lock_guard和std::unique_lock。 • std::atomic提供了原子变量更方便实现实现保护不需要使用互斥量 • std::call_once保证函数在多线程环境下只调用一次可用于实现单例。 • volatile常用于读写操作不可以被优化掉的内存中。 • std::condition_variable提供等待的同步机制可阻塞一个或多个线程等待其它线程通知后唤醒。 • std::future用于异步调用的包装和返回值。 • async更方便的实现了异步调用异步调用优先使用async取代创建线程。
C11 的异步操作-async
C11中增加了async如它的名字一样这个关键字就是用来创建异步操作的c11中有个更常用的异步操作叫做线程thread那么thread和async有什么区别呢以及async的优势是什么应该怎么使用
C11 使用 std::async创建异步程序
C11中增加了线程可以非常方便的创建线程它的基本用法是这样的
void f(int n);
std::thread t(f, n 1);
t.join();但是线程毕竟是属于比较低层次的东西有时候使用有些不便比如希望获取线程函数的返回结果的时候就不能直接通过 thread.join()得到结果这时就必须定义一个变量在线程函数中去给这个变量赋值然后join最后得到结果这个过程是比较繁琐的。
c11还提供了异步接口std::async通过这个异步接口可以很方便的获取线程函数的执行结果。std::async会自动创建一个线程去调用线程函数它返回一个std::future这个future中存储了线程函数返回的结果当需要线程函数的结果时直接从future中获取非常方便。
其实std::async提供的便利可不仅仅是这一点它首先解耦了线程的创建和执行可以在需要的时候获取异步操作的结果其次它还提供了线程的创建策略比如可以通过延迟加载的方式去创建线程可以以多种方式去创建线程。在介绍async具体用法以及为什么要用std::async代替线程的创建之前先看看std::future、std::promise和 std::packaged_task。
std::future
std::future是一个非常有用也很有意思的东西简单说std::future提供了一种访问异步操作结果的机制。从字面意思来理解 它表示未来因为一个异步操作是不可能马上就获取操作结果的只能在未来某个时候获取但是可以以同步等待的方式来获取结果可以通过查询future的状态future_status来获取异步操作的结果。future_status有三种状态 deferred异步操作还没开始 ready异步操作已经完成 timeout异步操作超时
//查询future的状态
std::future_status status;
do {status future.wait_for(std::chrono::seconds(1));if (status std::future_status::deferred) {std::cout deferred\n;} else if (status std::future_status::timeout) {std::cout timeout\n;} else if (status std::future_status::ready) {std::cout ready!\n;
} while (status ! std::future_status::ready);获取future结果有三种方式get、wait、wait_for其中get等待异步操作结束并返回结果wait只是等待异步操作完成没有返回值wait_for是超时等待返回结果。
std::promise
std::promise为获取线程函数中的某个值提供便利在线程函数中给外面传进来的promise赋值当线程函数执行完成之后就可以通过promis获取该值了值得注意的是取值是间接的通过promise内部提供的future来获取的。它的基本用法
std::promiseint pr;
std::thread t([](std::promiseint p){p.set_value_at_thread_exit(9);
},std::ref(pr));
std::futureint f pr.get_future();
auto r f.get();std::packaged_task
std::packaged_task它包装了一个可调用的目标如function, lambda expression, bind expression, or another function object,以便异步调用它和promise在某种程度上有点像promise保存了一个共享状态的值而packaged_task保存的是一 个函数。它的基本用法
std::packaged_taskint() task([](){ return 7; });
std::thread t1(std::ref(task));
std::futureint f1 task.get_future();
auto r1 f1.get();std::promise、std::packaged_task和std::future的关系
看了std::async相关的几个对象std::future、std::promise和std::packaged_task其中 std::promise和std::packaged_task的结果最终都是通过其内部的future返回出来的看看他们之间的关系到底是怎样的std::future提供了一个访问异步操作结果的机制它和线程是一个级别的属于低层次的对象在它之上高一层的是std::packaged_task和std::promise他们内部都有future以便访问异步操作结果std::packaged_task包装的是一个异步操作而std::promise包装的是一个值都是为了方便异步操作的因为有时需要获取线程中的某个值这时就用std::promise而有时需要获一个异步操作的返回值这时就用std::packaged_task。
那 std::promise和std::packaged_task之间又是什么关系呢说他们没关系也没关系说他们有关系也有关系都取决于如何使用他们了可以将一个异步操作的结果保存到std::promise中。
为什么要用std::async代替线程的创建
std::async是为了让开发者的少费点脑子的它让这三个对象默契的工作。大概的工作过程是这样的std::async先将异步操作用std::packaged_task包 装起来然后将异步操作的结果放到std::promise中这个过程就是创造未来的过程。外面再通过future.get/wait来获取这个未来的结果
现在来看看std::async的原型
async(std::launch::async | std::launch::deferred, f, args...) 第一个参数是线程的创建策略有两种策略默认的策略是立即创建线程
std::launch::async在调用async就开始创建线程。
std::launch::deferred延迟加载方式创建线程。调用async时不创建线程直到调用了future的get或者wait时才创建线程。
第二个参数是线程函数第三个参数是线程函数的参数。
std::async基本用法
std::futureint f1 std::async(std::launch::async, []() {return 8;});
cout f1.get() endl; //output: 8
std::futurevoid f2 std::async(std::launch::async, []() {cout 8 endl;//return 8;});
f2.wait(); //output: 8
std::futureint future std::async(std::launch::async, []() {std::this_thread::sleep_for(std::chrono::seconds(3));return 8;});
std::cout waiting...\n;
//Test12();
std::future_status status;
Sleep(3000);
do {status future.wait_for(std::chrono::seconds(1));if (status std::future_status::deferred) {std::cout deferred\n;}else if (status std::future_status::timeout) {std::cout timeout\n;}else if (status std::future_status::ready) {std::cout ready!\n;}
} while (status ! std::future_status::ready);
std::cout result is future.get() \n;可能的结果waiting… timeout timeout ready! result is 8
总结
std::async是更高层次上的异步操作它的存在可以使开发者不用关注线程创建内部细节就能方便的获取异步执行状态和结果还可以指定线程创建策略应该用std::async替代线程的创建让它成为做异步操作的首选。
C11新特性之智能指针
c11引入了三种智能指针 std::shared_ptr std::weak_ptr std::unique_ptr
shared_ptr
shared_ptr使用了引用计数每一个shared_ptr的拷贝都指向相同的内存每次拷贝都会触发引用计数1每次生命周期结束析构的时候引用计数-1在最后一个shared_ptr析构的时候内存才会释放。
使用方法如下
struct ClassWrapper {ClassWrapper() {cout construct endl;data new int[10];}~ClassWrapper() {cout deconstruct endl;if (data ! nullptr) {delete[] data;}}void Print() {cout print endl;}int* data;
};void Func(std::shared_ptrClassWrapper ptr) {ptr-Print();
}int main() {auto smart_ptr std::make_sharedClassWrapper();auto ptr2 smart_ptr; // 引用计数1ptr2-Print();Func(smart_ptr); // 引用计数1smart_ptr-Print();ClassWrapper *p smart_ptr.get(); // 可以通过get获取裸指针p-Print();return 0;
}智能指针还可以自定义删除器在引用计数为0的时候自动调用删除器来释放对象的内存代码如下
std::shared_ptrint ptr(new int, [](int *p){ delete p; });关于shared_ptr有几点需要注意
• 不要用一个裸指针初始化多个shared_ptr会出现double_free导致程序崩溃
• 通过shared_from_this()返回this指针不要把this指针作为shared_ptr返回出来因为this指针本质就是裸指针通过this返回可能 会导致重复析构不能把this指针交给智能指针管理。
class A {shared_ptrA GetSelf() {return shared_from_this();// return shared_ptrA(this); 错误会导致double free}
};尽量使用make_shared少用new。 不要delete get()返回来的裸指针。 不是new出来的空间要自定义删除器。 要避免循环引用循环引用导致内存永远不会被释放造成内存泄漏。
using namespace std;
struct A;
struct B;struct A {std::shared_ptrB bptr;~A() {cout A delete endl;}
};struct B {std::shared_ptrA aptr;~B() {cout B delete endl;}
};int main() {auto aaptr std::make_sharedA();auto bbptr std::make_sharedB();aaptr-bptr bbptr;bbptr-aptr aaptr;return 0;
}上面代码产生了循环引用导致aptr和bptr的引用计数为2离开作用域后aptr和bptr的引用计数-1但是永远不会为0导致指针永远不会析构产生了内存泄漏如何解决这种问题呢答案是使用weak_ptr。
weak_ptr
weak_ptr是用来监视shared_ptr的生命周期它不管理shared_ptr内部的指针它的拷贝的析构都不会影响引用计数纯粹是作为一个旁观者监视shared_ptr中管理的资源是否存在可以用来返回this指针和解决循环引用问题。
作用1返回this指针上面介绍的shared_from_this()其实就是通过weak_ptr返回的this指针。作用2解决循环引用问题。
struct A;
struct B;struct A {std::shared_ptrB bptr;~A() {cout A delete endl;}void Print() {cout A endl;}
};struct B {std::weak_ptrA aptr; // 这里改成weak_ptr~B() {cout B delete endl;}void PrintA() {if (!aptr.expired()) { // 监视shared_ptr的生命周期auto ptr aptr.lock();ptr-Print();}}
};int main() {auto aaptr std::make_sharedA();auto bbptr std::make_sharedB();aaptr-bptr bbptr;bbptr-aptr aaptr;bbptr-PrintA();return 0;
}输出
A
A delete
B deleteunique_ptr
std::unique_ptr是一个独占型的智能指针它不允许其它智能指针共享其内部指针也不允许unique_ptr的拷贝和赋值。使用方法和shared_ptr类似区别是不可以拷贝
using namespace std;struct A {~A() {cout A delete endl;}void Print() {cout A endl;}
};int main() {auto ptr std::unique_ptrA(new A);auto tptr std::make_uniqueA(); // error, c11还不行需要c14std::unique_ptrA tem ptr; // error, unique_ptr不允许移动ptr-Print();return 0;
}Lambda 表达式
Lambda 表达式实际上就是提供了一个类似匿名函数的特性而匿名函数则是在需要一个函数但是又不想费力去命名一个函数的情况下去使用的。
Lambda 表达式的基本语法如下
[ caputrue ] ( params ) opt - ret { body; };capture是捕获列表 params是参数表(选填) opt是函数选项可以填mutable,exception,attribute选填 mutable说明lambda表达式体内的代码可以修改被捕获的变量并且可以访问被捕获的对象的non-const方法。 exception说明lambda表达式是否抛出异常以及何种异常。 attribute用来声明属性。
ret是返回值类型拖尾返回类型。(选填)body是函数体。
捕获列表lambda表达式的捕获列表精细控制了lambda表达式能够访问的外部变量以及如何访问这些变量。 []不捕获任何变量。 []捕获外部作用域中所有变量并作为引用在函数体中使用按引用捕获。 []捕获外部作用域中所有变量并作为副本在函数体中使用(按值捕获)。注意值捕获的前提是变量可以拷贝且被捕获的量在 lambda 表达式被创建时拷贝而非调用时才拷贝。如果希望lambda表达式在调用时能即时访问外部变量我们应当用引用方式捕获。
int a 0;
auto f [] { return a; };a1;cout f() endl; //输出0int a 0;
auto f [a] { return a; };a1;cout f() endl; //输出1[,foo]按值捕获外部作用域中所有变量并按引用捕获foo变量。 [bar]按值捕获bar变量同时不捕获其他变量。 [this]捕获当前类中的this指针让lambda表达式拥有和当前类成员函数同样的访问权限。如果已经使用了或者就默认添加此选项。捕获this的目的是可以在lamda中使用当前类的成员函数和成员变量。
class A
{public:int i_ 0;void func(int x,int y){auto x1 [] { return i_; }; //error,没有捕获外部变量auto x2 [] { return i_ x y; }; //OKauto x3 [] { return i_ x y; }; //OKauto x4 [this] { return i_; }; //OKauto x5 [this] { return i_ x y; }; //error,没有捕获x,yauto x6 [this, x, y] { return i_ x y; }; //OKauto x7 [this] { return i_; }; //OK};int a0 , b1;
auto f1 [] { return a; }; //error,没有捕获外部变量
auto f2 [] { return a }; //OK
auto f3 [] { return a; }; //OK
auto f4 [] {return a; }; //error,a是以复制方式捕获的无法修改
auto f5 [a] { return ab; }; //error,没有捕获变量b
auto f6 [a, b] { return a (b); }; //OK
auto f7 [, b] { return a (b); }; //OK注意f4虽然按值捕获的变量值均复制一份存储在lambda表达式变量中修改他们也并不会真正影响到外部但我们却仍然无法修改它们。如果希望去修改按值捕获的外部变量需要显示指明lambda表达式为mutable。被mutable修饰的lambda表达式就算没有参数也要写明参数列表。
int a 0;
auto f1 [] { return a; }; //error
auto f2 [] () mutable { return a; }; //OK原因lambda表达式可以说是就地定义仿函数闭包的“语法糖”。它的捕获列表捕获住的任何外部变量最终会变为闭包类型的成员变量。按照C标准lambda表达式的operator()默认是const的一个const成员函数是无法修改成员变量的值的。而mutable的作用就在于取消operator()的const。
lambda表达式的大致原理
每当你定义一个lambda表达式后编译器会自动生成一个匿名类这个类重载了()运算符我们称为闭包类型closure type。那么在运行时这个lambda表达式就会返回一个匿名的闭包实例是一个右值。所以我们上面的lambda表达式的结果就是一个个闭包。对于复制传值捕捉方式类中会相应添加对应类型的非静态数据成员。在运行时会用复制的值初始化这些成员变量从而生成闭包。对于引用捕获方式无论是否标记mutable都可以在lambda表达式中修改捕获的值。至于闭包类中是否有对应成员C标准中给出的答案是不清楚的与具体实现有关。
lambda表达式是不能被赋值的
auto a [] { cout A endl; };
auto b [] { cout B endl; };a b; // 非法lambda无法赋值
auto c a; // 合法生成一个副本闭包类型禁用了赋值操作符但是没有禁用复制构造函数所以你仍然可以用一个lambda表达式去初始化另外一个lambda表达式而产生副本。
在多种捕获方式中最好不要使用[]和[]默认捕获所有变量。
默认引用捕获所有变量你有很大可能会出现悬挂引用Dangling references因为引用捕获不会延长引用的变量的生命周期
std::functionint(int) add_x(int x)
{return [](int a) { return x a; };
}上面函数返回了一个lambda表达式参数x仅是一个临时变量函数add_x调用后就被销毁了但是返回的lambda表达式却引用了该变量当调用这个表达式时引用的是一个垃圾值会产生没有意义的结果。上面这种情况使用默认传值方式可以避免悬挂引用问题。
但是采用默认值捕获所有变量仍然有风险看下面的例子
class Filter
{
public:Filter(int divisorVal):divisor{divisorVal}{}std::functionbool(int) getFilter() {return [](int value) {return value % divisor 0; };}private:int divisor;
};这个类中有一个成员方法可以返回一个lambda表达式这个表达式使用了类的数据成员divisor。而且采用默认值方式捕捉所有变量。你可能认为这个lambda表达式也捕捉了divisor的一份副本但是实际上并没有。因为数据成员divisor对lambda表达式并不可见你可以用下面的代码验证
// 类的方法下面无法编译因为divisor并不在lambda捕捉的范围
std::functionbool(int) getFilter()
{return [divisor](int value) {return value % divisor 0; };
}原代码中lambda表达式实际上捕捉的是this指针的副本所以原来的代码等价于
std::functionbool(int) getFilter() { return [this](int value) {return value % this-divisor 0; };}尽管还是以值方式捕获但是捕获的是指针其实相当于以引用的方式捕获了当前类对象所以lambda表达式的闭包与一个类对象绑定在一起了这很危险因为你仍然有可能在类对象析构后使用这个lambda表达式那么类似“悬挂引用”的问题也会产生。所以采用默认值捕捉所有变量仍然是不安全的主要是由于指针变量的复制实际上还是按引用传值。
lambda表达式可以赋值给对应类型的函数指针。但是使用函数指针并不是那么方便。所以STL定义在 functional 头文件提供了一个多态的函数对象封装std::function其类似于函数指针。它可以绑定任何类函数对象只要参数与返回类型相同。如下面的返回一个bool且接收两个int的函数包装器
std::functionbool(int, int) wrapper [](int x, int y) { return x y; };lambda表达式一个更重要的应用是其可以用于函数的参数通过这种方式可以实现回调函数。
最常用的是在STL算法中比如你要统计一个数组中满足特定条件的元素数量通过lambda表达式给出条件传递给count_if函数
int value 3;
vectorint v {1, 3, 5, 2, 6, 10};
int count std::count_if(v.beigin(), v.end(), [value](int x) { return x value; });再比如你想生成斐波那契数列然后保存在数组中此时你可以使用generate函数并辅助lambda表达式
vectorint v(10);
int a 0;
int b 1;
std::generate(v.begin(), v.end(), [a, b] { int value b; b b a; a value; return value; });
// 此时v {1, 1, 2, 3, 5, 8, 13, 21, 34, 55}当需要遍历容器并对每个元素进行操作时
std::vectorint v { 1, 2, 3, 4, 5, 6 };
int even_count 0;
for_each(v.begin(), v.end(), [even_count](int val){if(!(val 1)){ even_count;}
});
std::cout The number of even is even_count std::endl;大部分STL算法可以非常灵活地搭配lambda表达式来实现想要的效果。
C std::function
std::function是一个函数对象的包装器std::function的实例可以存储复制和调用任何可调用的目标包括 函数。 lamada表达式。 绑定表达式或其他函数对象。 指向成员函数和指向数据成员的指针。
当std::function对象没有初始化任何实际的可调用元素调用std::function对象将抛出std::bad_function_call异常。
std::function简介
类模版std::function是一种通用、多态的函数封装。std::function的实例可以对任何可以调用的目标实体进行存储、复制、和调用操作这些目标实体包括普通函数、Lambda表达式、函数指针、以及其它函数对象等。std::function对象是对C中现有的可调用实体的一种类型安全的包裹我们知道像函数指针这类可调用实体是类型不安全的。
通常std::function是一个函数对象类它包装其它任意的函数对象被包装的函数对象具有类型为T1, …,TN的N个参数并且返回一个可转换到R类型的值。std::function使用 模板转换构造函数接收被包装的函数对象特别是闭包类型可以隐式地转换为std::function。
C标准库详细说明了这个的基本使用http://www.cplusplus.com/reference/functional/function/.
这里我们大概总结一下。
Member types
成员类型说明result_type返回类型argument_type如果函数对象只有一个参数那么这个代表参数类型。first_argument_type如果函数对象有两个个参数那么这个代表第一个参数类型。second_argument_type如果函数对象有两个个参数那么这个代表第二个参数类型。
Member functions
成员函数声明说明constructor构造函数constructs a new std::function instancedestructor析构函数 destroys a std::function instanceoperator给定义的function对象赋值operator bool检查定义的function对象是否包含一个有效的对象operator()调用一个对象
std::function使用
封装普通函数例子
#include iostream#include vector#include list#include map#include set#include string#include algorithm#include functional#include memoryusing namespace std;typedef std::functionint(int) Functional;int TestFunc(int a) { return a; }int main(){ Functional obj TestFunc; int res obj(1); std::cout res std::endl; while(1); return 0;}封装lambda表达式
#include iostream#include vector#include list#include map#include set#include string#include algorithm#include functional#include memoryusing namespace std;typedef std::functionint(int) Functional;auto lambda [](int a)-int{return a;};int main(){ Functional obj lambda; res obj(2); std::cout res std::endl; while(1); return 0;}封装仿函数
#include iostream#include vector#include list#include map#include set#include string#include algorithm#include functional#include memoryusing namespace std;typedef std::functionint(int) Functional;class Functor{public: int operator()(int a) { return a; }};int main(){ Functor func; Functional obj func; res obj(3); std::cout res std::endl; while(1); return 0;}封装类的成员函数和static成员函数
#include iostream
#include vector
#include list
#include map
#include set
#include string
#include algorithm
#include functional
#include memoryusing namespace std;typedef std::functionint(int) Functional;class CTest
{
public:int Func(int a){return a;}static int SFunc(int a){return a;}
};int main()
{CTest t; obj std::bind(CTest::Func, t, std::placeholders::_1); res obj(3); cout member function : res endl; obj CTest::SFunc; res obj(4); cout static member function : res endl; while(1);return 0;
}关于可调用实体转换为std::function对象需要遵守以下两条原则
转换后的std::function对象的参数能转换为可调用实体的参数可调用实体的返回值能转换为std::function对象的返回值。
std::function对象最大的用处就是在实现函数回调使用者需要注意它不能被用来检查相等或者不相等但是可以与NULL或者nullptr进行比较。
为什么要用std::function
好用并实用的东西才会加入标准的。因为好用实用我们才在项目中使用它。std::function实现了一套类型消除机制可以统一处理不同的函数对象类型。以前我们使用函数指针来完成这些现在我们可以使用更安全的std::function来完成这些任务。
参考文档
C std::function技术浅谈 右值引用和move语义
先看一个简单的例子直观感受下
string a(x); // line 1
string b(x y); // line 2
string c(some_function_returning_a_string()); // line 3如果使用以下拷贝构造函数
string(const string that)
{size_t size strlen(that.data) 1;data new char[size];memcpy(data, that.data, size);
}以上3行中只有第一行(line 1)的x深度拷贝是有必要的因为我们可能会在后边用到xx是一个左值(lvalues)。
第二行和第三行的参数则是右值因为表达式产生的string对象是匿名对象之后没有办法再使用了。
C 11引入了一种新的机制叫做“右值引用”以便我们通过重载直接使用右值参数。我们所要做的就是写一个以右值引用为参数的构造函数
string(string that) // string is an rvalue reference to a string
{
data that.data;
that.data 0;
}我们没有深度拷贝堆内存中的数据而是仅仅复制了指针并把源对象的指针置空。事实上我们“偷取”了属于源对象的内存数据。由于源对象是一个右值不会再被使用因此客户并不会觉察到源对象被改变了。在这里我们并没有真正的复制所以我们把这个构造函数叫做“转移构造函数”move constructor他的工作就是把资源从一个对象转移到另一个对象而不是复制他们。
有了右值引用再来看看赋值操作符
string operator(string that)
{
std::swap(data, that.data);
return *this;
}注意到我们是直接对参数that传值所以that会像其他任何对象一样被初始化那么确切的说that是怎样被初始化的呢对于C 98答案是复制构造函数但是对于C 11编译器会依据参数是左值还是右值在复制构造函数和转移构造函数间进行选择。
如果是ab这样就会调用复制构造函数来初始化that因为b是左值赋值操作符会与新创建的对象交换数据深度拷贝。这就是copy and swap 惯用法的定义构造一个副本与副本交换数据并让副本在作用域内自动销毁。这里也一样。
如果是a x y这样就会调用转移构造函数来初始化that因为xy是右值所以这里没有深度拷贝只有高效的数据转移。相对于参数that依然是一个独立的对象但是他的构造函数是无用的trivial因此堆中的数据没有必要复制而仅仅是转移。没有必要复制他因为xy是右值再次从右值指向的对象中转移是没有问题的。
总结一下复制构造函数执行的是深度拷贝因为源对象本身必须不能被改变。而转移构造函数却可以复制指针把源对象的指针置空这种形式下这是安全的因为用户不可能再使用这个对象了。
下面我们进一步讨论右值引用和move语义。
C98标准库中提供了一种唯一拥有性的智能指针std::auto_ptr该类型在C11中已被废弃因为其“复制”行为是危险的。
auto_ptrShape a(new Triangle);auto_ptrShape b(a);注意b是怎样使用a进行初始化的它不复制triangle而是把triangle的所有权从a传递给了b也可以说成“a 被转移进了b”或者“triangle被从a转移到了b”。
auto_ptr 的复制构造函数可能看起来像这样简化
auto_ptr(auto_ptr source) // note the missing const
{
p source.p;
source.p 0; // now the source no longer owns the object
}auto_ptr 的危险之处在于看上去应该是复制但实际上确是转移。调用被转移过的auto_ptr 的成员函数将会导致不可预知的后果。所以你必须非常谨慎的使用auto_ptr 如果他被转移过。
auto_ptrShape make_triangle()
{return auto_ptrShape(new Triangle);
}auto_ptrShape c(make_triangle()); // move temporary into c
double area make_triangle()-area(); // perfectly safeauto_ptrShape a(new Triangle); // create triangle
auto_ptrShape b(a); // move a into b
double area a-area(); // undefined behavior显然在持有auto_ptr 对象的a表达式和持有调用函数返回的auto_ptr值类型的make_triangle()表达式之间一定有一些潜在的区别每调用一次后者就会创建一个新的auto_ptr对象。这里a 其实就是一个左值lvalue的例子而make_triangle()就是右值rvalue的例子。
转移像a这样的左值是非常危险的因为我们可能调用a的成员函数这会导致不可预知的行为。另一方面转移像make_triangle()这样的右值却是非常安全的因为复制构造函数之后我们不能再使用这个临时对象了因为这个转移后的临时对象会在下一行之前销毁掉。
我们现在知道转移左值是十分危险的但是转移右值却是很安全的。如果C能从语言级别支持区分左值和右值参数我就可以完全杜绝对左值转移或者把转移左值在调用的时候暴露出来以使我们不会不经意的转移左值。
C 11对这个问题的答案是右值引用。右值引用是针对右值的新的引用类型语法是X。以前的老的引用类型X 现在被称作左值引用。
使用右值引用X作为参数的最有用的函数之一就是转移构造函数X::X(X source)它的主要作用是把源对象的本地资源转移给当前对象。
C 11中std::auto_ptr T 已经被std::unique_ptr T 所取代后者就是利用的右值引用。
其转移构造函数
unique_ptr(unique_ptr source) // note the rvalue reference
{ptr source.ptr;source.ptr nullptr;
}这个转移构造函数跟auto_ptr中复制构造函数做的事情一样但是它却只能接受右值作为参数。
unique_ptrShape a(new Triangle);
unique_ptrShape b(a); // error
unique_ptrShape c(make_triangle()); // okay第二行不能编译通过因为a是左值但是参数unique_ptr source只能接受右值这正是我们所需要的杜绝危险的隐式转移。第三行编译没有问题因为make_triangle()是右值转移构造函数会将临时对象的所有权转移给对象c这正是我们需要的。
转移左值
有时候我们可能想转移左值也就是说有时候我们想让编译器把左值当作右值对待以便能使用转移构造函数即便这有点不安全。出于这个目的C 11在标准库的头文件 utility 中提供了一个模板函数std::move。实际上std::move仅仅是简单地将左值转换为右值它本身并没有转移任何东西。它仅仅是让对象可以转移。
以下是如何正确的转移左值
unique_ptrShape a(new Triangle);
unique_ptrShape b(a); // still an error
unique_ptrShape c(std::move(a)); // okay请注意第三行之后a不再拥有Triangle对象。不过这没有关系因为通过明确的写出std::move(a)我们很清楚我们的意图亲爱的转移构造函数你可以对a做任何想要做的事情来初始化c我不再需要a了对于a您请自便。
当然如果你在使用了mova(a)之后还继续使用a那无疑是搬起石头砸自己的脚还是会导致严重的运行错误。
总之std::move(some_lvalue)将左值转换为右值可以理解为一种类型转换使接下来的转移成为可能。
一个例子
class Foo
{unique_ptrShape member;public:Foo(unique_ptrShape parameter): member(parameter) // error{}};上面的parameter其类型是一个右值引用只能说明parameter是指向右值的引用而parameter本身是个左值。Things that are declared as rvalue reference can be lvalues or rvalues. The distinguishing criterion is: if it has a name, then it is an lvalue. Otherwise, it is an rvalue.
因此以上对parameter的转移是不允许的需要使用std::move来显示转换成右值。