19. std::move语义
并不移动任何东西,唯一功能是将一个左值强制转化为右值引用,继而可通过右值引用使用该值,以用于移动语义。本质是一个无条件static_cast,形参为左值类型,返回右值引用。
1) 函数原型定义
template <typename T>
typename remove_reference<T>::type&& move(T&& t) {
return static_cast<typename remove_reference<T>::type&&>(t);
}
将对象的状态或所有权从一个对象转移到另一个对象,只是转移,没有内存搬迁或内存拷贝,可提高利用效率,改善性能。编译期计算,无运行时开销。
原理:首先,通过右值引用传递模板实现,利用引用折叠原理将右值经过T&&传递类型保持不变还是右值,而左值经过T&&变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变(左值还是右值,需要进行类型推导)。然后通过static_cast<>进行强制类型转换返回T&&右值引用,static_cast之所以能使用类型转换,是通过remove_refrence::type模板移除T&&,T&的引用,获取具体类型T。
示例:
template<typename T>
void f(T&& param);
f(10); // 10是右值
int x = 10;
f(x); // x是左值
2) 操作说明
l 可move的操作均为形参可接受右值引用的函数
l unique_ptr,调用move才可以赋值,赋值后原指针被置为空
l
stl(std::array、std::vector等)、std::string等具有move构造和赋值的可使用move。使用比如vector::push_back等这类函数时,会对参数对象连数据也会复制.这就造成对象内存的额外创建,本意把参数push_back进去就行。而通过std::move,可避免不必要的拷贝操作l 对象没有move构造和赋值则退化为copy.
l move会阻碍 RVO返回值优化;
std::string s0 = "hello";
std::string str = s0 + " world.";
s0 + " world." 的结果是个临时对象, 这个表达式是个右值,那么 str 对象的构造函数在 C++11 中,会优先调用 Move Ctor .
3) 移动构造和移动赋值
如果需要某个类支持移动操作,需要实现移动构造和移动赋值操作符,两者都具有移动语义,应该同时出现或者禁止。
// 同时出现
class AA {
public: …
AA(AA&&);
AA& operator=(AA&&);
…
};
// 同时禁止
class AA {
public:
AA(AA&&) = delete;
AA& operator=(AA&&) = delete;
};
-
禁止操作const对象
const对象不能修改,自然无法移动。std::move 会把对象转换成右值引用类型,极少有类型会定义以const右值引用为参数的移动构造函数和赋值操作符,因此实际往往退化成对象拷贝而不是对象移动,带来了性能上的损失。
std::string g_string; std::vector<std::string> g_stringList;
void func() {
const std::string myString = "String content";
g_string = std::move(myString); //复制
const std::string anotherString = "Another string content";
g_stringList.push_back(std::move(anotherString)); // 复制
}
注:如果不需要拷贝/移动函数,请明确禁止。
20. 类型推导auto,decltype
1) auto
auto的自动类型推导,用于从初始化表达式中推断出变量的数据类型。通过auto的自动类型推导,可以大大简化编程工作
l auto 可以避免编写冗长、重复的类型名,也可保证定义变量时初始化
l auto 类型推导规则复杂,需要仔细理解
l 如能使代码更清晰,继续使用明确的类型,且只在局部变量使用 auto
如果没有注意 auto 类型推导时忽略引用,可能引入难以发现的性能问题:
std::vectorstd::string v;
auto s1 = v[0]; // auto 推导为 std::string,拷贝 v[0]
使用auto定义接口,如头文件中的常量,可能因为修改了值,而导致类型发生变化
推导规则复杂示例:
auto a = 3; // int
const auto ca = a; // const int
const auto& ra = a; // const int&
auto aa = ca; // int, 忽略 const 和 reference
auto ila1 = { 10 }; // std::initializer_list
auto ila2{ 10 }; // std::initializer_list
auto&& ura1 = x; // int&
auto&& ura2 = ca; // const int&
auto&& ura3 = 10; // int&&
const int b[10];
auto arr1 = b; // const int*
auto& arr2 = b; // const int(&)[10]
2) decltype
为解决复杂的类型声明而使用的关键字,称作decltype类型说明符。有点像auto的反函数,auto可以让你声明一个变量,而decltype则可以从一个变量或表达式中得到类型
作用于变量、表达式及函数名。
l 作用于变量直接得到变量的类型;
l 作用于表达式,结果是左值的表达式得到类型的引用,结果是右值的表达式得到类型;decltype不会去真的求解表达式的值
l 作用于函数名会得到函数类型,不会自动转换成指针。
示例
using FuncType = int(int &, int); // 声明了一个函数类型
int add_to(int &des, int ori); // 下面的函数就是上面的类型
decltype(add_to) *pf = add_to; //使用decltype获得函数add_to的类型
int a = 4;
pf(a, 2); // 通过函数指针调用add_to
21. 类型后置
常用的函数返回类型一般是在前面即void function();。但是C++11新增了语法,函数返回类型声明后置,在函数名和参数列表后面返回指定类型。C++函数返回类型后置一般用于模板函数中,可以提前返回编译器还没编译开始的类型。
//编译器在遇到f3的参数列表前,T和U还不在作用域内,因此在参数列表后使用decltypetemplate<typename T, typename U>
auto f3(T t, U u)->decltype(TU) //decltype强制类型转换,转换为(TU)类型
{ return t*u;}
返回类型后置语法,将decltype和auto结合起来完成返回值类型的推导
在原来放返回值类型的位置写auto,在函数声明结束以后接一个’->'再跟着写函数的返回值类型。两种方式的效果是一样的。
一般情况下,当函数要返回多个数据时,会选择将数据封装在类(或结构体)中返回,或者直接返回一个指针。这两种方式要么麻烦,要么没有办法取得数据的数量。其实真正的需求就是返回一个数组(可以计算维度的)指针。这种方法是存在的:
int (*getResultArray(int mode))[10];
通过上面的形式,声明了一个返回包含10个整数的数组的指针。可以像数组一样使用:
auto arr = getResultArray(1);
cout << sizeof(*arr)/sizeof(**arr) << endl; //可以正确计算维度。
有经验的程序员会这么做:
typedef int arr10[10];
arr10* getResultArray(int mode)
C++11增加了返回类型后置以后: auto getResultArray(int mode)->int(*)[10];
22. 强制类型转换
类型转化机制可分为隐式类型转换和显示类型转化(强制类型转换)。
隐式类型转换较常见,在混合类型表达式中经常发生。避免类型转换,我们在代码的类型设计上应该考虑到每种数据的数据类型是什么,而不是应该过度使用类型转换来解决问题。
示例:
int i=3;
double j = 3.1;
i+j;//i会被转换成double类型,后才做加法运算。
四种强制类型转换操作符:static_cast、dynamic_cast、const_cast、reinterpret_cast,尽量少使用转型操作,尤其是dynamic_cast,耗时较高,会导致性能的下降,尽量使用其他方法替代。
1) static_cast
编译时期的静态类型检查: static_cast < type-id > ( expression )
相当于传统C语言的强制转换,运算符把expression转换为type-id类型,用来强迫隐式转换如non-const对象转为const对象,编译时检查(指针越界、类型检查),其操作数相对是安全的。用于非多态的转换,可以转换指针及其他,但无运行时类型检查来保证转换的安全性。主要有如下几种用法:
①用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换
进行上行转换是安全的;进行下行转换时,由于没有动态类型检查,所以是不安全的
②用于基本数据类型之间的转换,如把int转换成char,把int转换成enum
③把空指针转换成目标类型的空指针
④把任何类型的表达式转换成void类型
注意:static_cast不能转换掉expression的const、volatile、或__unaligned属性
示例:
int e = 10;
const int f = static_cast<const int>(e);//将int型转换成const int
const int g = 20;
int *h = static_cast<int*>(&g);//编译错误,不能转换掉g的const属性
2) dynamic_cast
运行时的检查, 要尽量避免使用。用于在集成体系中进行安全的向下转换downcast(基类指针/引用->派生类指针/引用). 源类中必须要有虚函数,保证多态,才能使用dynamic_cast(expression).
dynamic_cast 是4个转换中唯一的RTTI操作符,提供运行时类型检查,如果不能转换返回NULL。dynamic_cast的出现一般说明基类和派生类设计出现了问题,派生类破坏了基类的契约,不得不通过 dynamic_cast 转换到子类进行特殊处理,建议改善类的设计,避免使用dynamic_cast来进行转换。
转换方式:
dynamic_cast< type* >(e) type是一个类类型且必须是一个有效的指针
dynamic_cast< type& >(e) type是一个类类型且必须是一个左值
dynamic_cast< type&& >(e) type必须是一个类类型且必须是一个右值
如果一条dynamic_cast语句的转换目标是指针类型并且失败了,则结果为0。如果转换目标是引用类型并且失败,则dynamic_cast运算符将抛出一个std::bad_cast异常(定义在typeinfo标准库头文件中)。e也可是一个空指针,结果是所需类型的空指针。
void f(const Base &b){
try{
const Derived &d = dynamic_cast<const Base &>(b); //使用b引用的Derived对象
}
catch(std::bad_cast){
//处理类型转换失败的情况
}
}
3) const_cast
移除对象的 const和volatile性质,使其可以修改;会破坏数据的不变性,建议少用。
// 不好的例子
const int i = 1024;
int* p = const_cast(&i);
*p = 2048; // 未定义行为
4) reinterpret_cast
用于转换不相关类型,尝试用reinterpret_cast将一种类型强制转换另一种类型,这破坏了类型的安全性与可靠性,是一种不安全的转换。不同类型之间尽量避免转换。
23. typdef,#define,using
23.1 typedef, #define
#define是预处理命令,在预处理是执行简单的替换,不做正确性的检查
typedef是在编译时处理的,它是在自己的作用域内给已经存在的类型一个别名
typedef (int*) pINT;
#define pINT2 int*
效果相同?实则不同,实践中见差别:pINT a,b;的效果同int *a; int *b;表示定义了两个整型指针变量。而pINT2 a,b;的效果同int *a, b;表示定义了一个整型指针变量a和整型变量b。
23.2 using与 typedef
C++11之前可通过typedef定义类型的别名。之后,用using实现声明别名
l 配合命名空间,对命名空间权限进行管理
using namespace std;//释放整个命名空间到当前作用域
using std::cout; //释放某个变量到当前作用域
l 类型重命名
作用等同typedef,但是逻辑上更直观。
l 继承体系中,改变部分接口的继承权限。
应用场景,比如需要私有继承一个基类,然后又想将基类中的某些public接口在子类对象实例化后对外开放直接使用。如下即可
class BaseA:private Base {
public:
using Base::dis1;//需要在BaseA的public下释放才能对外使用
void dis2show() {
this->dis2();
}
};
两者区别
typedef Type Alias;
using Alias = Type;
模板别名
template<class T> using MyAllocatorVector = std::vector<T, MyAllocator<T>>;
MyAllocatorVector<int> data; // 使用 using 定义的别名
template<class T> class MyClass {
private: MyAllocatorVector<int> data_; // 模板类中使用
}
// 通过模板包装 typedef,需要实现一个模板类
template<class T>
struct MyAllocatorVector {
typedef std::vector<T, MyAllocator<T>> type;
};
MyAllocatorVector<int>::type data; // 多写 ::type
template<class T>
class MyClass {
private:
typename MyAllocatorVector<int>::type data_; // 模板类中需加上::type,typename
};
注:使用using导入命名空间会影响后续代码,易造成符号冲突,所以不要在头文件以及源文件中的#include之前使用using导入命名空间。
24. const, #define
24.1 const 和 constexpr
1)欲阻止一个变量被改变,可以使用const修饰,定义该const变量时,通常需对它进行初始化,因为后续不能再去改变;
class Bto {
public:
Bto (int length) : dataLength(length) {}
private:
const int dataLength;
};
2)对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
3)在一个函数声明中,const可修饰形参,在函数内部不能改变其值;
void PrintFoo(const Foo& foo)
4)对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量, 不会调用其它非const的成员函数
class Tup { public:
// ...
int PrintValue() const { // 不会修改成员变量
std::cout << value << std::endl;
}
int GetValue() const { // 不会修改成员变量
return value;
}
private: int value;
};
5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。
注:const 数据成员只在某个对象生存期内是常量,对于整个类而言却是可变的,因为类可创建多个对象,不同对象其 const 数据成员的值可以不同。不能在类声明中初始化 const 数据成员,const 数据成员的初始化在类构造函数的初始化表中进行…,
constexpr
生成常量表达式,需要编译器支持,允许程序利用编译时的计算能力。
#define MAX_MSISDN_LEN 20 // 不好
const int MAX_MSISDN_LEN = 20; // 好, const常量
constexpr int MAX_MSISDN_LEN = 20; //C++11以上版本,可以使用
允许函数被应用在以前调用宏的所有场合。譬如,想要一个计算数组size的函数,size是10的倍数。用constexpr,就可调用一个constexpr函数去声明一个数组。如果不用,则需要创建一个宏或者使用模板,因为不能用函数的返回值去声明数组的大小。
constexpr int getDefaultArraySize (int multiplier){
return 10 * multiplier;
}
int my_array[ getDefaultArraySize(3)];
限制:
函数中只能有一个return语句(有极少特例),只能调用其它constexpr函数,只能使用全局constexpr变量。假如将一成员函数标记为constexpr,则顺带也将它标记成了const。如果将一个变量标记为constexpr,则同样它是const的。但相反并不成立,即一个const的变量或函数,并不是constexpr.
24.2 const与 #define
1)#define定义的常量没有数据类型,所给出的是一个立即数;const定义的常量有类型名字,存放在静态区域。编译器可以对后者进行类型安全检查,而对前者只进行字符替换,没有类型安全检查,且在字符替换可能会产生意料不到的错误(边际效应)。
2)处理阶段不同,#define定义的宏变量在预处理时进行替换,可能有多个拷贝,const所定义的变量在编译时确定其值,在程序运行过程中内存中只有一个拷贝。
3)#define定义的常量是不可以用指针去指向,const定义的常量可以用指针去指向该常量的地址
4)#define可以定义简单的函数,const不可以定义函数
5)有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。
6)希望某些常量只在类中有效。由于#define 定义的宏常量是全局的,不能达到目的,于是想当然地觉得应该用 const 修饰数据成员来实现。
- 函数inline内联
内联用一种类似于宏的展开的方式代替方法调用,在调用方法内部将被调用方法展开,可提高性能。指明内联机制的意图有两种:一种是用保留字 inline给方法的定义加上前缀;另一种方法是在头部声明中定义方法。使用内联函数的目的是为了提高函数的运行效率。一般函数进行调用时,要将程序执行权转到被调用函数中,然后再返回到调用它的函数中,而内联函数在调用时,是将调用表达式用内联函数体来替换。
逻辑上,编译器为内联一个方法所使用的过程:内联方法的连续代码块被复制到调用方法的调用点处。内联方法内的任何局部变量在块内分配,内联方法的输入参数和返回值被映射到调用方法的局部变量空间。如果内联方法有多个返回点,则这些返回点就变成内联块尾部的分支,所有与调用(对于与创建新块相关联的SP修改可能例外)有关的痕迹及随之而来的所有性能损失都被消除了。
内联原则
l 内联函数省去调用的时间是以代码膨胀为代价的,内联函数体代码不能过长。只适合于1~10行的小函数。对一个含有许多语句的大函数,函数调用和返回的开销相对来说微不足道,也没必要用内联实现,一般的编译器会放弃内联,而采用普通的方式调用函数。
l 内联函数一般不建议包含循环语句,常规来说复杂的循环操作要比调用函数的开销大。
l 内联函数要做参数类型检查, 这是跟宏相比的优势。
l 如果内联函数包含复杂的控制结构,如循环、分支(switch)、try-catch 等语句,一般编译器将该函数视同普通函数。
l 虚函数、递归函数通常不被用来做内联函数。
l 不应内联其实现容易发生改变的方法。
… … …
惟一和微小总是意味着代码尺寸和性能方面的收益。惟一正好是由于使用频率才内联的,只有一个调用点的事实说明不需要考虑惟一方法的尺寸和调用频率,内联合成后的代码将比以前更小更快。微小可以在不考虑使用频率的情况下进行内联,通常少于4条源代码级语句的方法,被编译成10条以下的汇编指令,要确保内联后的微小方法仍保持微小(内联非叶方法时,任何由叶方法扩充所产生的扩充量都要翻倍)。
25.1 内联的性能增益
避免昂贵方法调用所带来的性能增益只是内联在性能方面的一半,另一半则是调用间的优化。一个经过良好优化的编译器会使内联方法块边界的任何痕迹变得认不出来。通过优化,方法的大部分将不再存在。
l 避免对输入参数进行范围检测的过度性保护,可减少代码尺寸,清除条件判断和分支;
l 编译器能在编译时确定方法的重要输入参数,那或许能进行一种十分划算的优化。
25.2 过度内联的影响
过度进行方法内联将带来代码数量级的膨胀,对性能产生负面影响,如缓存失败和页面错误,这将使所假设的任何主要收获变得微小。另一方面,过分内联的程序将执行较少的指令,但是需要更长的时间来执行。
内联代码在每次内联时都各不相同,所以在从另一个内联代码位置再次执行该方法时,缓存将再次丢失。即使内联代码相同,内联指令也将出现在进程代码空间的不同地址中。导致的缓存性能降低可能掩盖与调用和返回开销相关的性能提高,特别对于大型方法更是如此。
25.4 内联的相关技巧
l 条件内联:使用编译行参数向编译器传递宏定义,输入参数用于定义名为INLINE的宏。把所有潜在要内联的方法的定义与要外联的方法划分开。外联方法包括在标准 .c 文件中,潜在要内联的方法放在 .inl 文件中。如要内联 .inl文件中的方法,可在编译命令中使用 –D 选项来定义 INLINE。此种内联技术是一种要么全有要么全无的方法,可简化早期开发阶段的调试工作。
l 选择性内联:给方法提供两个版本,一种是内联的,另外一种是外联的。在 .c 文件中原来版本的代码体替换成对内联方法的调用。通常只在性能重要的路径中内联方法调用
l 递归内联:直接递归方法不能内联,作为一般的规则,特别是在性能十分重要的情况下,只要迭代方案相当简单,就应注意使用迭代方案代替递归方案。是一种让人别扭但对提高递归方法性能有效的方式。
- static的作用
static作用域
1)函数体内: static 修饰的局部变量作用范围为该函数体,不同于auto变量,其内存只被分配一次,因此其值在下次调用的时候维持了上次的值
2)模块内:static修饰全局变量或全局函数,可以被模块内的所有函数访问,但是不能被模块外的其他函数访问,使用范围限制在声明它的模块内
3)类中
修饰成员变量,表示该变量属于整个类所有,对类的所有对象只一份拷贝
修饰成员函数表示该函数属于整个类,不接受this指针,只能访问类中的static成员变量。
静态全局变量限制了其作用域, 即只在定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可避免在其它源文件中引起错误。
使用匿名 namespace封装或用static修饰
对cpp文件中不需导出的变量,常量或函数,可匿名 namespace封装或用static修饰。
静态函数成员变量,静态成员函数,静态全局变量,静态函数局部变量,每一种都有特殊的处理。static只能保证变量,常量和函数的文件作用域,但namespace还可封装类型等。统一namespace来处理C++的作用域,而不需同时使用static和namespace来管理。static修饰的函数不能用来实例化模板,而匿名namespace可以。
- std::function对象包装
类模板std::function是可调用对象的包装器,可包装除了类成员之外的所有可调用对象。包括普通函数,函数指针,lambda,仿函数。通过指定的模板参数,可用统一方式保存,并延迟执行(回调)。
示例一:
#include <iostream>
#include <functional>
using namespace std;
class functor {
public:
void operator()() {
cout<<__FUNCTION__<<endl;
}
};
class A {
public:
A(const function<void()> & cb):_callback(cb)
{}
void notify() {
_callback();
}
function<void()> _callback;
};
int main(int argc, char *argv[]){
functor fct;
A a(fct);
a.notify();
return 0;
}
- Lambda, 闭包
匿名函数是许多编程语言都支持的概念,有函数体,没有函数名。匿名函数最常用的是作为回调函数的值。c++引入了lambda 函数, lambda表达式类似Javascript中的闭包,可用于创建并定义匿名的函数对象,以简化编程工作。
函数对象能维护状态,但相对来说语法开销大,而函数指针语法开销小,却没法保存范围内的状态。lambda函数结合了两者的优点,可写出优雅简洁的代码。
capture->return-type {body}
auto func = [] () { cout << “hello,world”; };
func(); // now call the function
28.1 变量捕获与lambda闭包实现
lambda函数能够捕获lambda函数外的具有自动存储时期的变量,函数体与这些变量的集合合起来叫闭包。
l [] 不截取任何变量
l [&} 截取外部作用域中所有变量,并作为引用在函数体中使用
l [=] 截取外部作用域中所有变量,并拷贝一份在函数体中使用
l [=, &foo] 截取外部作用域中所有变量,并拷贝一份在函数体中使用,但对foo变量使用引用
l [bar] 截取bar变量并且拷贝一份在函数体重使用,同时不截取其他变量
l [x, &y] x按值传递,y按引用传递
l [this] 截取当前类中的this指针。如果已经使用了&或者=就默认添加此选项
lambda通过创建个小类来实现。这个类重载了操作符(),一个lambda函数是该类的一个实例。当该类被构造时,周围的变量就传递给构造函数并以成员变量保存起来。看起来跟函数对象很相似。
lambda函数的类型是std:function
#include <iostream>
#include <functional>
using namespace std;
int main() {
function<int()> func = []()->int{
return 3;
};
cout << func() << endl;
return 0;
}
28.2 avoid default capture modes
l 如果lambda和函数都可以的场景,优先使用函数
函数无法捕获局部变量或在局部范围内声明;如果需要这些东西,尽可能选择 lambda,而不是手写的 functor。另一方面,lambda和functor不会重载, 如果需要重载,则使用函数。 如果 lambda 和函数都可以的场景,则优先使用函数;尽可能使用最简单的工具
示例
// 编写一个只接受 int 或 string 的函数,重载是自然选择
void F(int);
void F(const string&);
// 需要捕获局部状态,或出现在语句或表达式范围,lambda是自然选择
vector v = LotsOfWork();
for (int taskNum = 0; taskNum < max; ++taskNum) {
pool.Run([=, &v] {…});
}
pool.Join();
l 非局部范围使用lambdas ,避免按引用捕获
非局部范围使用lambdas包括返回值,存储在堆上,或传递给其它线程。局部的指针和引用不应该在它们的范围外存在。 lambdas按引用捕获就是把局部对象的引用存储起来。如果这会导致超过局部变量生命周期的引用存在,则不应该按引用捕获。
// 不好
void Foo() {
int local = 42;
// 按引用捕获 local.
//函数返回后,local不再存在,因此Process()的行为未定义!
threadPool.QueueWork([&]{ Process(local); });
}
// 好
void Foo() {
int local = 42; // 按值捕获,Process()调用过程中,local 总是有效
threadPool.QueueWork([=]{ Process(local); });
}
l 如果捕获this ,则显式捕获所有变量
在成员函数中的 [=] 看起来是按值捕获。但因为是隐式的按值获取了this指针,并能够操作所有成员变量,数据成员实际是按引用捕获的,一般情况下建议避免。如果的确需要这样做,明确写出对 this 的捕获。
class MyClass {
public: void Foo() {
int i = 0;
auto Lambda = = { Use(i, data_); }; // 不好: 像是按值捕获,成员变量实际按引用捕获
data_ = 42;
Lambda(); // 调用 use(42);
data_ = 43;
Lambda(); // 调用 use(43);
auto Lambda2 = i, this { Use(i, data_); }; // 好,显式按值捕获,避免混淆
}
private: int data_ = 0;
};
l 避免使用默认捕获模式
lambda表达式提供了两种默认捕获模式:按引用(&)和按值(=)。 默认按引用捕获会隐式的捕获所有局部变量的引用,易导致访问悬空引用。相比之下,显式的写出需要捕获的变量可以更容易的检查对象生命周期,减小犯错。默认按值捕获会隐式的捕获this指针,且难以看出lambda函数所依赖的变量是哪些。如果存在静态变量,还会让阅读者误以为lambda拷贝了一份静态变量。因此,通常应明确写出需要捕获的变量,而不是使用默认捕获模式。
错误示例
auto func() {
int addend = 5;
static int baseValue = 3;
return = { // 实际只复制了addend
++baseValue; // 修改会影响静态变量的值
return baseValue + addend;
};
}
正确示例
auto func() {
int addend = 5;
static int baseValue = 3;
return addend, baseValue = baseValue mutable { //用C++14捕获,拷贝一份变量
++baseValue; // 修改的拷贝,不会影响静态变量的值
return baseValue + addend;
};
}
- std::bind
bind()函数的意义就像它的函数名一样,用来绑定函数调用的某些参数, 实际是一种延迟计算的思想,将可调用对象保存起来,然后在需要的时候再调用。而且这种绑定是非常灵活的,不论是普通函数、函数对象、还是成员函数都可以绑定,而且其参数可以支持占位符,比如可以这样绑定一个二元函数auto f = bind(&func, _1, _2);,调用的时候通过f(1,2)实现调用。
代码示例:
auto bindFunc1 = bind(TestFunc, std::placeholders::_1, ‘A’, 100.1);
bindFunc1(10);
- 锁的机制及管理
线程之间的锁包含:互斥锁、条件锁、自旋锁、读写锁等。一般而言,锁的功能越强大,性能就会越低。
读写锁
可借助于“读者-写者”问题进行理解。计算机中某些数据被多个进程共享,对数据库的操作有两种:一种是读操作,就是从数据库中读取数据而不会修改;另一种是写操作,会修改数据库中存放的数据。因此,可得到允许在数据库上同时执行多个“读”,但某一时刻只能在数据库上有一个“写”操作来更新数据。通常有些公共数据修改的机会很少,但其读的机会很多。并且在读的过程中会伴随着查找,给这种代码加锁会降低我们的程序效率。读写锁(写独占,读共享,写锁优先级高)可以解决这个问题。
所谓「读写锁」,就是同时可以被多个读者拥有,但是只能被一个写者拥有的锁。而所谓「多个读者、单个写者」,并非指程序中只有一个写者(线程),而是说不能有多个写者同时去写。STL 和 Boost 都提供了 shared_mutex 来解决。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CRQGIKN3-1628759960644)(file:///D:/写文章-CSDN博客_files/af0b4435814d42aa867f610c56c841ea.png)]
示例:(计数器)
class Counter {
public:
Counter() : value_(0) {
}
// Multiple threads/readers can read the counter's value at the same time.
std::size_t Get() const {
std::shared_lock<std::shared_mutex> lock(mutex_);
return value_;
}
// Only one thread/writer can increment/write the counter's value.
void Increase() {
// You can also use lock_guard here.
std::unique_lock<std::shared_mutex> lock(mutex_);
value_++;
}
// Only one thread/writer can reset/write the counter's value.
void Reset() {
std::unique_lock<std::shared_mutex> lock(mutex_);
value_ = 0;
}
private:
mutable std::shared_mutex mutex_;
std::size_t value_;
};
shared_mutex 比一般的 mutex 多了函数 lock_shared() / unlock_shared(),允许多个(读者)线程同时加锁、解锁,而 shared_lock 则相当于共享版的 lock_guard。对 shared_mutex 使用 lock_guard 或 unique_lock 就达到了写者独占的目的。
测试代码:
std::mutex g_io_mutex;
void Worker(Counter& counter) {
for (int i = 0; i < 3; ++i) {
counter.Increase();
std::size_t value = counter.Get();
std::lock_guard<std::mutex> lock(g_io_mutex);
std::cout << std::this_thread::get_id() << ' ' << value << std::endl;
}
}
int main() {
const std::size_t SIZE = 2;
Counter counter;
std::vector<std::thread> v;
v.reserve(SIZE);
v.emplace_back(&Worker, std::ref(counter));
v.emplace_back(&Worker, std::ref(counter));
for (std::thread& t : v) {
t.join();
}
return 0;
}
输出(仍然是随机性的):对于计数器来说,原子类型std::atomic<> 也许是更好选择
读写场景
假如一个线程,先作为读者用 shared_lock 加锁,读后又想变成写者,处理方式:
方法一:先解读锁,再加写锁。问题是,一解一加之间,其他写者说不定已经介入并修改了数据,那么当前线程作为读者时所持有的状态(比如指针、迭代器)也就不再有效。
方法二:用upgrade_lock(仅限Boost,STL未提供),可以当做shared_lock用,但必要时可直接从读「升级」为写。
{
// acquire shared ownership to read.
boost::upgrade_lock<boost::shared_mutex> upgrade_lock(shared_mutex_);
// read ......
// upgrade to exclusive ownership to write.
boost::upgrade_to_unique_lock<boost::shared_mutex> unique_lock(upgrade_lock);
// write......
}
锁的管理
锁管理遵循RAII(Resource Acquisition Is Initialization)习语来处理资源。锁管理器在构造函数中自动绑定它的互斥体,并在析构函数中释放它,大大减少死锁的风险。锁管理器包含:用于简单的std::lock_guard,及用于高级用例的std::unique_lock。
std:: lock_guard
{
std::mutex m,
std::lock_guard<std::mutex> lockGuard(m);//生命周期只在这{}里面有效
sharedVariable = getVar();
}
std::unique_lock
比std::lock_guard更强大,在lock_guard的基础上还能:
l 没有关联互斥体时创建
l 没有锁定的互斥体时创建
l 显式和重复设置或释放关联互斥锁
l 移动互斥体 move
l 尝试锁定互斥体
l 延迟锁定关联互斥体
… …
std::mutex用来保证线程同步,防止不同的线程同时操作同一个共享数据。使用mutex不安全,当一个线程在解锁前异常退出,那么其它被阻塞的线程就无法继续下去。lock_guard相对安全,基于作用域,能自解锁,当该对象创建时,它会像m.lock()一样获得互斥锁,生命周期结束时会自动析构(unlock),不会因为某个线程异常退出而影响其他线程。
- volatile
volatile所定义的变量随时都有可能改变,因此,编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取。如果没有volatile关键字,编译器可能优化读取和存储,可能暂时使用寄存器中的值,如这个变量由别的程序更新了,将出现不一致的现象。
-
多线程应用中被多个任务共享的变量
-
防止编译器优化:中断信号的控制,直接从内存中读取数据,而非寄存器
short flag;
void test() {
do1();
while (flag == 0);
do2();
}
上段程序等待内存变量flag的值变为1,之后才运行do2()。flag的值由别的程序更改,可能是某个硬件中断服务程序。如:当某个按钮按下,就会对DSP产生中断,在按键中断程序中修改flag为1,上面程序就能得以继续运行。但编译器并不知道flag会被别的程序修改,因此在它进行优化的时候,可能会把flag的值先读入某个寄存器,然后等待那个寄存器变为1。如不幸进行了这样的优化,那while循环就变成了死循环,因寄存器的内容不可能被中断服务程序修改。为让程序每次都读取真正flag的值,就需要定义为如下形式:
volatile short flag;
基本说明
l 不在两个操作之间把volatile变量缓存在寄存器。在多任务、中断、甚至setjmp环境下,变量可能被其它程序改变,编译器无法知道,volatile告诉编译器这种情况。
l 不做常量合并、常量传播等优化。像下面的代码,if的条件不会当作无条件真。
volatile int i = 1;
if (i > 0 ) …
l 对volatile变量的读写不会被优化。如果对一个变量赋值但后面没用到,编译器常可省略那个赋值操作,然而对Memory Mapped IO的处理是不能这样优化。
Volatile不保证对内存操作的原子性。其一,x86需要LOCK前缀才能在SMP下保证原子性;其二,RISC根本不能对内存直接运算,要保证原子性得用别的方法,如atomic_inc。
31.1 volatile常用场景
1).中断服务程序中修改的供其它程序检测的变量需加volatile
2).多任务环境下各个任务之间共享的标志应加volatile
3).存储器映射的硬件寄存器通常也要加volatile,因每次对它的读写都可能有不同意义;
另外,以上几种情况常要同时考虑数据的完整性(相互关联的几个标志读了一半被打断重写),在1)中可通过关中断来解决;2)中可以禁止任务调度;3)依靠硬件的良好设计。
31.2 编译器优化
内存访问速度远不及CPU处理速度,为提高机器整体性能,在硬件上引入硬件高速缓存Cache,加速对内存的访问。另外,现代CPU中指令执行并不一定严格按照顺序执行,没有相关性的指令可乱序执行,以充分利用CPU的指令流水线,提高执行速度。
编译器优化常用方法:将内存变量缓存到寄存器,调整指令顺序充分利用CPU指令流水线,常见的是重新排序读写指令。由编译器优化或硬件重排序引起的问题的解决办法是在从硬件(或其他处理器)的角度看必需以特定顺序执行的操作之间设置内存屏障。
void Barrier(void)
这个函数通知编译器插入一个内存屏障,但对硬件无效,编译后的代码会把当前CPU寄存器中的所有修改过的数值存入内存,需要这些数据的时候再重新从内存中读出。
31.3 volatile指针
指针和普通变量一样,有时也有变化程序的不可控性。常见例子:子中断服务子程序修改一个指向一个 buffer 的指针时,必须用 volatile 来修饰这个指针。 和const类似,const有常量指针和指针常量,volatile 也有相应的概念:
volatile char* vpch;
char* volatile pchv;
指针是一种普通的变量,从访问上没有什么不同于其他变量的特性。其保存的数值是个整型数据,和整型变量不同的是,这个整型数据指向的是一段内存地址。
说 明:
-
可把一个非volatile int赋给volatile int,但不能把非volatile对象赋给一个volatile对象
-
除了基本类型外,对用户定义类型也可以用volatile类型进行修饰
-
C++中一个有volatile标识符的类只能访问它接口的子集,一个由类的实现者控制的子集。也只能用const_cast来获得对类型接口的完全访问。此外,volatile像const一样会从类传递到它的成员。
-
std::thread线程
std::thread使用std的thread实例化一个线程对象。
1).默认构造函数,创建一个空的 thread 执行对象。
2).初始化构造函数,创建一个 thread对象,该 thread对象可被 joinable,新产生的线程会调用 fn 函数,该函数的参数由 args 给出。
3).拷贝构造函数(被禁用),意味着 thread 不可被拷贝构造。
4). move 构造函数,调用成功之后 x 不代表任何 thread 执行对象。
注:可被joinable的thread对象必须在销毁之前被主线程join或将其设置为detached.
代码片段参考:
//栈上
thread t1(show); //根据函数初始化执行
thread t2(show);
//线程数组
thread th[2]{thread(show), thread(show) };
array<thread, 3> threads = { thread(show), thread(show), thread(show) };
//堆上
thread *pt1(new thread(show));
thread *pt2(new thread(show));
//线程指针数组
thread *pth(new thread[2]{thread(show), thread(show) });
32.1 join & detach
thread::join()让主线程等待直到该子线程执行结束,线程对象执行了join后就不再joinable,所以只能调用join一次。
thread::detach
用来和线程对象分离,这样线程可独立执行,不过由于没有thread对象指向该线程而失去了对它的控制,当对象析构时线程会继续在后台执行,但是当主程序退出时并不能保证线程能执行完。如果没有良好的控制机制或者这种后台线程比较重要,最好不用detach而应该使用join。
detach脱离主线程的绑定,子线程会成为孤儿线程,线程间将无法通信。主线程挂了,子线程不报错,子线程执行完自动退出
32.2 move 赋值操作
thread& operator= (thread&& rhs) noexcept;
thread& operator= (const thread&) = delete;// thread对象不可被拷贝
示例:
#include
#include
using namespace std;
void fun1(int n) { //初始化构造函数
cout << "Thread " << n << " executing\n";
n += 10;
this_thread::sleep_for(chrono::milliseconds(10));
}
void fun2(int & n) {//拷贝构造函数
cout << "Thread " << n << " executing\n";
n += 20;
this_thread::sleep_for(chrono::milliseconds(10));
}
int main() {
int n = 0;
thread t1; //t1不是一个thread
thread t2(fun1, n + 1); //按照值传递
t2.join();
cout << "n=" << n << '\n';
n = 10;
thread t3(fun2, ref(n)); //引用
thread t4(move(t3)); //t4执行t3,t3被销毁
t4.join();
cout << "n=" << n << '\n';
return 0;
}
运行结果:
Thread 1 executing
n=0
Thread 10 executing
n=30
32.3 lambda与多线程
代码参考示例:
#include
using namespace std;
int main() {
auto fun = [](const char *str) {cout << str << endl; };
thread t1(fun, "hello world!");
thread t2(fun, "hello beijing!");
return 0;
}
32.4线程安全和非安全
线程安全指多线程访问时,采用加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用,不会出现数据不一致或数据污染。
线程非安全指未提供数据访问保护,可能多个线程先后更改数据最终所得为脏数据。
以下参考代码线程非安全:
#include
using namespace std;
const int N = 100000000;
int num = 0;
void run() {
for (int i = 0; i < N; i++){
num++;
}
}
int main() {
clock_t start = clock();
thread t1(run);
thread t2(run);
t1.join();
t2.join();
clock_t end = clock();
cout << "num=" << num << ",用时 " << end - start << " ms" << endl;
return 0;
}
线程安全示例:使用原子变量
#include
atomic_int num{ 0 };//无线程冲突,线程安全
- struct,union,bitfield
33.1 struct和union
结构体struct:将不同类型的数据组合成一个整体
共同体union:不同类型的几个变量共同占用一段内存
1)结构体中的每个成员都有自己独立的地址,它们是同时存在的
共同体中的所有成员占用同一段内存,不能同时存在
2)sizeof(struct)是内存对齐后所有成员长度的总和,sizeof(union)是内存对齐后最长数据成员的长度。
33.2 union与bit field
用法示例:如果仅仅使用状态位,可以使用位域和联合一起来使用,但需要注意字节序问题。
union BF {
unsigned short bf; //16bit
struct {
unsigned char c1:2;
unsigned char c2:2;
unsigned char c3:4;
unsigned char c4:8;
}st;
BF() {
bf =0x1b0d;
}
};
在BF联合体构造方法中 ,bf =0x1b0d .则bf的二进制形式为:0001 1011 0000 1101.
对于64bit x64 小端机器,st结构成员应该从c1 ->c4 从由右向左对应。则:
unsigned char c1:2= 0x01; //十进制:1
unsigned char c2:2=0x11; //十进制: 3
unsigned char c3:4=0x0000 //十进制:0
unsigned char c4:8=0x 0001 1011 //十进制:27
在BF联合里sizeof(bf) ==sizeof(st) 长度相同,操作st就等同于操作bf的位域。
说明:结构体中定义的成员默认是public,而C++类中的成员默认是private的,但可声明public,private 和 protected;结构体和类对象都必须使用new创建;
性。并且应该在这