赞
踩
在模板类型推导过程中
template<typename T> void f(T& param); int x = 27; const int cx = x; const int& rx = x; f(x); // 此处T的类型被推导为int,param为int& f(cx); // 此处T的类型被推导为const int,param为const int& f(rx); // 此处T的类型被推导为const int,param为const int& template<typename T> void f(T* param); int x = 27; const int* px = &x; f(&x); // T的类型为int,param为int* f(px); // T的类型为const int,param为const int*
template<typename T>
void f(T&& param)
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // x为左值,因此T的类型为int&,param为int&
f(cx); // cx为左值,因此T的类型为const int&,param为const int&
f(rx); // rx为左值,因此T的类型为const int&,param为const int&
f(27); // 27为右值,因此T的类型为int,param为int&&
template<typename T>
void f(T param); // param按值传递
const char* const ptr = "F"; // ptr是指向const对象的const指针
f(ptr); // T的类型为const char*,param为const char*,此时按值传递,将存在拷贝过程,拷贝过程中指针的const或volatile特性会被忽略,此处针对的是指针的修饰
void fun(int param[]) // 函数形参无法被声明为真正的数组类型,此处声明仍然会按照指针形式处理,同void fun(int* param)
void fun(int& param) // 即int (&)[]
编译器会利用表达式来推导类型和参数类型。
一般情况下,auto类型推导和模板类型推导相同,但auto类型推导会假定用大括号内的初始化表达式表示一个std::initializer_list,但模板类型推导却不会。
auto x = 27; // 等价于 // template<typename T> // void func_for_x(T param); // func_for_x(27); const auto cx = x; // 等价于 // template<typename T> // void func_for_cx(const T param); // func_for_cx(x); const auto& rx = x; // 等价于 // template<typename T> // void func_for_rx(const T& param); // func_for_rx(x); const char name[] = "R.N.Briggs"; auto arr1 = name; // arr1 类型为const char* auto& arr2 = name; // arr2 类型为const char (&)[13] void someFunc(int, double); // 函数类型为void(int, double) auto func1 = someFunc; // func1类型为void (*)(int, double)
在函数返回值或lambda式形参中使用auto,意思是使用模板类型推导而非auto类型推导。
auto x = {11, 23, 9}; // x类型为 std::initializer_list<int>
template<typename T> // 带有形参的模板,与x的声明等价
void f(T param);
f({11, 23, 9}); // 此处无法推导T的类型,报错
template<typename T>
void f(std::initializer_list<T> initlist);
f({11, 23, 9}); // T的类型推导为int,从而initlist类型为std::initializer_list<int>
auto createInitList() { return {1, 2, 3}; } // 无法为{1, 2, 3}完成类型推导
std::vector<int> v;
auto resetV = [&V](const auto& newValue) { v = newValue; };
resetV({1, 2, 3}); // 无法为{1, 2, 3}完成类型推导
绝大多数情况下,decltype会得出变量或表达式的类型而不作任何修改。
const int i = 0; // decltype(i)为const int
bool f(const Widget& w); // decltype(w)为const Widget&,decltype(f)为bool(const Widget&)
Widget w;
f(w); // decltype(f(w))为bool
vector<int> v; // decltype(v[0])为int&
对于类型为T的左值表达式,除非该表达式仅有一个名字,decltype总是得出类型T&
template<typename Container, typename Index> auto authAndAccess(Container& c, Index i) -> decltype(c[i]) { authenticateUser(); return c[i]; } // 改进:C++14 template<typename Container, typename Index> auto authAndAccess(Container& c, Index i) { authenticateUser(); return c[i]; } std::deque<int> d; authAndAccess(d, 5) = 10; // 此处编译无法通过,原因为此函数模板返回类型推导结果为int,此结果的原因是因为auto推导时会忽略引用,则此处右值赋值右值无法通过编译 // 改进:于C++14基础上,将auto替换为decltype(auto) template<typename Container, typename Index> decltype(auto) authAndAccess(Container& c, Index i) { authenticateUser(); return c[i]; } // 无法向该函数传递右值容器 // 改进:于C++14基础上,增加万能引用,允许Container进行左值或右值绑定 template<typename Container, typename Index> decltype(auto) authAndAccess(Container&& c, Index i) { authenticateUser(); return c[i]; } // 改进:于C++14基础上,在return上增加std::forward<Container>的使用 template<typename Container, typename Index> decltype(auto) authAndAccess(Container& c, Index i) { authenticateUser(); return std::forward<Container>(c)[i]; }
C++14 支持decltype(auto),和auto一样,它会从其初始化表达式推导类型,但是它的类型推导使用的是decltype的规则。
int x = 0; // decltype(x)为int,decltype((x))为int&
使用auto声明定义变量或函数的优点:
能够避免未初始化变量及复杂的变量声明
能够直接定义闭包。使用std::function<>声明的闭包和auto声明的区别:
能够避免“类型捷径”。
std::vector<int> v;
unsigned sz = v.size(); // 此声明可能导致sz内存不足
auto sz = v.size(); // sz的类型是std::vector<int>::size_type
使用auto声明定义的问题:
隐式代理类型可以导致auto根据初始化表达式推导出错误的类型。
带显式类型定义的初始化习惯用法强制auto推导出目标类型。
auto index = static_cast<int>(d * c.size());
{}初始化可以应用的语境最为广泛,可以阻止隐式窄化类型转换,还对最令人苦恼的解析(most vexing parse)语法免疫。
// 非静态成员指定默认初始值
class Widget
{
...
private:
int x{0}; // 可行
int y = 0; // 可行
int z(0); // 不可行
};
// 不可拷贝对象
std::atomic<int> ai1{0}; // 可行
std::atomic<int> ai2(0); // 可行
std::atomic<int> ai3 = 0; // 不可行
{}初始化禁止内建类型之间进行隐式窄化类型转换;()不会
double x, y, z;
int sum1{x + y + z}; // 报错,double类型之和无法用int表示
int sum2(x + y + z); // 没问题,等同于sum2 = x + y + z
{}对最令人苦恼解析(most vexing parse)语法免疫
Widget w1(10); // 调用Widget构造函数,传入形参10
Widget w2(); // 这个语句声明了一个名为w2,返回一个Widget类型对象的函数
Widget w3{}; // 调用没有形参的Widget构造函数
在构造函数重载判定的时候,{}初始化会强烈地优先与带有std::initializer_list类型的形参的构造函数相匹配,即使其他重载版本有着更加匹配的形参列表。
// 此类构造函数的形参中没有任何一个具备std::initializer_list类型 class Widget { public: Widget(int, bool); Widget(int, double); ... }; Widget w1(10, true); // 调用的是第一个构造函数 Widget w2{10, true}; // 调用的是第一个构造函数 Widget w3(10, 5.0); // 调用的是第二个构造函数 Widget w3{10, 5.0}; // 调用的是第二个构造函数 // 此类反之 class Widget { public: Widget(int, bool); Widget(int, double); Widget(std::initializer_list<long double>); ... }; Widget w1(10, true); // 调用的是第一个构造函数 Widget w2{10, true}; // 调用的是第三个构造函数,10以及true被强制转换为double类型 // 常规会执行复制或移动的构造函数也可能被带有std::initializer_list类型形参的构造函数替代 class Widget { public: Widget(int, bool); Widget(int, double); Widget(std::initializer_list<long double>); operator float() const; ... }; Widget w0{10, 5.}; Widget w1(w0); // 调用的是复制构造函数 Widget w2{w0}; // 调用的是带有std::initializer_list类型形参的构造函数(w0的返回值被强制转换为float,后float又被强制转换为long double) Widget w3(std::move(w0)); // 调用的是移动构造函数 Widget w4{std::move(w0)}; // 过程及结果同w2 // 窄化类型转换错误 class Widget { public: Widget(int, bool); Widget(int, double); Widget(std::initializer_list<bool>); ... }; Widget w{10, 5.}; // 报错,std::initializer_list<>中禁止窄化类型转换 // 找不到
使用()还是{},会造成结果大相径庭的一个例子是:使用两个实参来创建一个std::vector<类型> 对象。std::make_unique及std::make_shared
std::vector<int> v1(10, 20); // 调用了形参中没有任何一个具备std::initializer_list类型的构造函数,结果是创建了一个
// 含有10个元素的std::vector所有的元素的值都是20
std::vector<int> v2{10, 20}; // 调用了形参中含有std::initializer_list类型的构造函数,结果是创建了一个含有2个元素
// 的std::vector元素的值分别是10和20
在模板内容进行对象创建时,使用()或是{}需要做出选择。如std::vector<>逐个定义值时使用{}
相对于0或NULL,优先选用nullptr。
void f(int);
void f(bool);
void f(void*);
f(0); // 调用的是f(int),而不是f(void*)
f(NULL); // 可能通不过编译,但一般会调用f(int),必不会调用f(void*)
f(nullptr); // 调用的是f(void*)
避免在整型和指针类型之间重载。
typedef不支持模板化,但别名声明支持
// 使用别名声明 template<typename T> using MyAllocList = std::list<T, MyAlloc<T>>; MyAllocList<Widget> lw; // 使用类型定义 template<typename T> struct MyAllocList { typedef std::list<T, MyAlloc<T>> type; }; MyAllocList<Widget>::type lw; // 则在模板内使用此定义创建一个链表,且容纳的对象类型由模板形参指定的话,需要加上typename template<typename T> class Widget { private: typename MyAllocList<T>::type list; // 带依赖类型 }; // 反之若使用别名模板定义,则不需要加上typename template<typename T> class Widget { private: MyAllocList<T> list; };
别名模板可以让人免写“::type”后缀,并且在模板内,对于内嵌typedef的引用经常要求加上typename前缀,表示其为一个类型名字
// 使用C++11类型特征变换工具时,由于这些工具是由嵌套在模板化的struct里的typedef实现的,因此在使用如std::remove_const<T>::type时需要加上前缀typename
std::remove_const<T>::type; // 由const T生成T
std::remove_reference<T>::type; // 由T&或T&&生成T
std::add_lvalue_reference<T>::type; // 由T生成T&
// C++14中对于此做了别名声明优化
std::remove_const<T>::type; // 由const T生成T
std::remove_const_t<T>; // 等价于
// template<class T>
// using remove_const_t = typename remove_const<T>::type;
// 下同
std::remove_reference<T>::type; // 由T&或T&&生成T
std::remove_reference_t<T>;
std::add_lvalue_reference<T>::type; // 由T生成T&
std::add_lvalue_reference_t<T>;
C++98风格的枚举类型,称为不限范围的枚举类型
限定作用域的枚举类型仅在枚举类型内可见。它们只能通过强制类型转换以转换至其他类型
// C++98风格的枚举类型中定义的枚举量的名字属于包含着这个枚举类型的作用域,这就表示在相同作用域内不能有其他实体去取用相同名字
enum color { black, white, red }; // black、white、red所在作用域和Color相同
auto white = false; // 报错,white已在范围内被声明过
// C++11强枚举类型语法
enum class Color { black, white, red }; // black、white、red所在作用域被限定在Color内
auto white = false; // 编译通过
Color c = white; // 报错,Color中无white枚举量
Color c = Color::white; // 编译通过
auto c = Color::white; // 编译通过,且符合2.1
限定作用域的枚举类型和不限范围的枚举类型都支持底层类型指定。限定作用域的枚举类型的默认底层类型是int,而不限范围的枚举类型没有默认底层类型(编译器会根据最大枚举量选择最小底层类型进行取值)
限定作用域的枚举类型总是可以进行前置声明,而不限范围的枚举类型却只有在制定了默认底层类型的前提下才可以进行前置声明。
优先选用删除函数,而非private未定义函数
C++98为了阻止这些函数被使用,采取的做法是声明其为private,并且不去定义它们。但是这种做法存在弊端,在其类成员函数或友元函数中仍然可以访问这些声明为private的函数,导致在链接阶段缺少函数定义而失败
举例,一个istream对象表示的是一个输入值流,其中可能有一部分已被读取,而有一部分未来有要读取的可能。若对istream对象进行拷贝,那是不是需要拷贝所有已经读取过的值以及所有未来将要读取的值呢?
template<class charT, class traits = char_traits<charT>>
class basic_ioss : public ios_base
{
public:
...
private:
basic_ios(const basic_ios&);
basic_ios& operator=(const basic_ios&);
};
C++11使用”=delete“将拷贝构造函数及拷贝赋值运算符标识为删除函数。习惯上,将删除函数声明为public。任何函数都能成为删除函数,但只有成员函数能声明成private
template<class charT, class traits = char_traits<charT>>
class basic_ioss : public ios_base
{
public:
...
basic_ios(const basic_ios&) = delete;
basic_ios& operator=(const basic_ios&) = delete;
};
任何函数都可以删除,包括非成员函数和模板具现
为意在改写的函数添加override声明
成员函数引用饰词使得对于左值和右值对象(*this)的处理能够区分开来
class Widget { public: using DataType = std::vector<double>; ... DataType& data() & { return values; } // 对于左值Widgets类型,返回左值。此版本仅在*this为左值时调用 DataType data() && { return std::move(values); } // 对于右值Widgets类型,返回右值。此版本尽在*this为右值时调用 ... private: DataType values; }; Widget w; // 对象 Widget makeWidget(); // 工厂函数 auto vals1 = w.data(); // 调用Widget::data左值重载版本vals1采用拷贝构造完成初始化 auto vals2 = makeWidget().data(); // 调用Widget::data右值重载版本vals2采用移动构造完成初始化
(C++11)优先选用const_iterator,而非iterator
在最通用的代码中,优先选用(自己实现的)非成员函数版本的begin、end及rbegin等,而非其成员函数版本,因为有些容器不支持cbegin等成员
// 非成员函数版本cbegin
template<class C>
auto cbegin(const C& container) -> decltype(std::begin(container))
{
return std::begin(container);
}
constexpr对象都具备const属性,并由编译期已知的值完成初始化
int sz; // 非constexpr变量
constexpr auto arraySize1 = sz; // 报错,sz的值在编译期未知
std::array<int, sz> data1; // 报错,一样的问题
constexpr auto arraySize2 = 10; // 正常,10是编译期常量
std::array<int, arraySize2> data2; // 正常,arraySize2是个constexpr
const auto arraySize = sz; // 正常,arraySize是sz的一个const副本
std::array<int, arraySize> data; // 报错,arraySize的值非编译期已知
constexpr函数在调用时若传入的实参值是编译期已知的,则会产出编译期结果,反之,则产出运行期结果同普通函数
比起非constexpr对象或constexpr函数而言,constexpr对象或是constexpr函数可以用在一个作用域更广的语境中
特别成员函数是指那些C++会自行生成的成员函数:默认构造函数、析构函数、拷贝操作以及移动操作
移动操作仅当类中未包含用户显式声明的复制操作、移动操作以及析构函数时才生成
拷贝构造函数仅当类中不包含用户显式声明的拷贝构造函数时才生成,如果该类声明了移动操作则拷贝构造函数将被删除。拷贝赋值运算符仅当类中不包含用户显式声明的拷贝赋值运算符才生成,如果该类声明了移动操作则拷贝赋值运算符将被删除。在已经存在显式声明的析构函数的条件下,生成拷贝操作已经成为了被废弃的行为
成员函数模板在任何情况下都不会抑制特别成员函数的生成
// 编译器会始终生成Widget的拷贝和移动操作
class Widget
{
template<typename T>
Widget(const T& rhs);
template<typename T>
Widget& operator=(const T& rhs);
};
std::unique_ptr是小巧、高速的、具备只移类型的智能指针,对托管资源实施专属所有权语义
默认地,资源析构采用delete运算符来实现,但可以指定自定义删除器。有状态的删除器和采用函数指针实现的删除器会增加std::unique_ptr类型的对象尺寸
class Investment {...};
class Stock : public Investment {...};
class Bond : public Investment {...};
class RealEstate : public Investment {...};
template<typename... Ts>
std::unique_ptr<Investment>
makeInvestment(Ts&&... params);
{
...
auto pInvestment = makeInvestment(arguments);
}// 作用域结束后*pInvestment析构
默认析构器和自定义析构器的区别:
auto delInvmt = [](Investment* pInvestment) { makeLogEntry(pInvestment); delete pInvestment; }; template<typename... Ts> std::unique_ptr<Investment, decltype(delInvmt)> // 若为C++14可使用函数类型推导,auto makeInvestment(Ts&&... params) { std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt); if(/* 应创建一个Stock类型的对象 */) { pInv.reset(new Stock(std::forward<Ts>(params)...)); // C++11禁止将裸指针直接赋值于智能指针,因为会触发隐式转换从而发生错误 } else if(/* 应创建一个Bond类型对象 */) { pInv.reset(new Bond(std::forward<Ts>(params)...)); } else if(/* 应创建一个RealEstate类型的对象 */) { pInv.reset(new RealEstate(std::forward<Ts>(params)...)); } return pInv; } void delInvmt2(Investment* pInvestment) { makeLogEntry(pInvestment); delete pInvestment; } template<typename... Ts> std::unique_ptr<Investment, void(*)(Investment*)> makeInvestment(Ts&&... params); // 返回值尺寸等于Investment*的尺寸加上至少函数指针的尺寸
两种形式地构造,这样地区分所指涉到地对象种类不会产生二义性:
将std::unique_ptr转换成std::shared_ptr是容易实现的,反之则不可
std::shared提供方便的手段,实现了任意资源在共享所有权语义下进行生命周期管理的垃圾回收
与std::unique_ptr相比,std::shared_ptr的尺寸通常是裸指针尺寸的两倍,它还会带来控制块的开销,并要求原子化的引用计数操作
默认的资源析构通过delete运算符进行,但同时也支持自定义析构器。析构器的类型不属于std::shared_ptr类型的一部分
auto loggingDel = [](Widget* pW)
{
makeLogEntry(pW);
delete pW;
};
std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel);
std::shared_ptr<Widget> spw(new Widget, loggingDel); //析构器类型不是std::shared_ptr类型的一部分
避免使用裸指针类型的变量来创建std::shared_ptr指针
// 这些规则会导致未定义行为
auto pw = new Widget;
...
std::shared_ptr<Widget> spw1(pw, loggingDel); // 为*pw创建一个控制块
...
std::shared_ptr<Widget> spw2(pw, loggingDel); // 为*pw创建了第二个控制块
// 这里创建了两个控制块,得到了两个引用计数,而每个引用计数最终都会归零,从而导致*pw被析构两次,第二次析构将会引发未定义行为
替代手法:使用std::make_shared。若采用了自定义析构器,就无法使用std::make_shared
若必须将一个裸指针传递给std::shared_ptr的构造函数,就直接传递new运算符的结果,而非传递一个裸指针变量
std::shared_ptr<Widget> spw1(new Widget, loggingDel);
std::shared_ptr<Widget> spw2(spw1); // 不会产生上述问题
托管到std::shared_ptr的类能够安全地由this指针创建一个std::shared_ptr(和this指针指涉到相同对象的std::shared_ptr)时,它将为你继承而来的基类提供一个模板,此模板设计模式为奇妙递归模板模式
std::vector<std::shared_ptr<Widget>> processWidgets; class Widget : public std::enable_shared_from_this<Widget> { public: ... void process(); ... }; void Widget::process() { ... processWidgets.emplace_back(shared_from_this()); ... } // 为了实现这个,必须有一个已经存在的指涉到当前对象的std::shared_ptr。若这样的对象不存在,该行为未定义,则shared_from_this抛出异常 // 为了避免调用者在std::shared_ptr指涉到该对象前就调用了引发shared_from_this的成员函数,继承自std::enable_shared_from_this的类通常会将其构造函数声明为private访问层级,并且允许调用者通过调用返回std::shared_ptr工厂函数来创建对象 class Widget : public std::enable_shared_from_this<Widget> { public: template<typename... Ts> static std::shared_ptr<Widget> create(Ts&&... params); ... void process(); ... private: ... // 构造函数 };
std::weak_ptr不能够申请,也不能检查是否为空,因为它仅是std::shared_ptr的一种扩充。一般通过std::shared_ptr创建,当使用std::shared_ptr完成初始化std::weak_ptr时,两者就指涉到相同位置,且std::weak_ptr不会影响std::shared_ptr所指涉到的对象的引用计数,但会影响弱计数
auto spw = std::make_shared<Widget>(); // 构造完成后,此时指涉到Widget的引用计数为1
std::weak_ptr<Widget> wpw(spw); // 与spw指涉到同一个对象,且spw引用计数保持为1
spw = nullptr; // 引用计数为0,Widget对象被析构,wpw空悬,即失效(expired)
// 检测失效的方式,两种形式
// 1.std::weak_ptr::lock,它会返回一个std::shared_ptr,若std::weak_ptr已失效,则std::shared_ptr为空
std::shared_ptr<Widget> spw1 = wpw.lock(); // 若wpw失效,则spw1为空
auto spw2 = wpw.lock(); // 同上
// 2.std::weak_ptr作为实参构造std::shared_ptr,若std::weak_ptr已失效,则抛出异常
std::shared_ptr<Widget> spw3(wpw); // 若wpw失效,抛出std::bad_weak_ptr类型的异常
使用std::weak_ptr来代替可能空悬的std::shared_ptr
std::weak_ptr可能的用武之地包括缓存,观察者列表,以及避免std::shared_ptr指针环路
// 第一种:工厂模式。 // 创建一个带缓存的工厂函数 std::unique_ptr<const Widget> loadWidget(WidgetId id); std::shared_ptr<const Widget> fastLoadWidget(WidgetID id) { static std::unordered_map<WidgetID, std::weak_ptr<const Widget>> cache; auto objPtr = cache[id].lock(); if(!objPtr) { objPtr = loadWidget(id); cache[id] = objPtr; } return objPtr; } // 存在弊端,由于相应的Widget不再使用,缓存中失效的std::weak_ptr可能会不断积累。这个实现可被优化,但不妨考虑其他设计模式 // 第二种:观察者模式。主要组件是主题和观察者,每个主题包含一个数据成员,该成员持有指涉到其观察者的指针。主题不会控制其观察者的生存期(不关心何时被析构),但需要确认的话,当一个观察者被析构之后,主题不会去访问它。此时可让每个主题持有一个容器来放置指涉到其观察者的std::weak_ptr,以便主题在使用某个指针之前,能够先确定他是否空悬
C++11加入了std::make_shared,但没有std::make_unique,C++14中加入了标准库
C++11实现std::make_unique。切记不要将自定义版本放入std命名空间,若版本变更为C++14,容易产生冲突
// 此函数模板不支持数组以及自定义析构器
template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&&...params)
{
return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}
make系列函数总共三个:std::make_unique、std::make_shared、std::allocate_shared(与std::make_shared一样,只不过它的第一个实参是个用以动态分配内存的分配器对象)
相比于直接使用new表达式,make系列函数消除了重复代码、改进了异常安全性,并且对于std::make_shared及std::allocated_shared而言,生成的目标代码会尺寸更小、速度更快
代码重复:源代码中的重复会增加编译次数,导致臃肿的目标代码,并且通常会产生更难上手的代码存根,通常会演化成不一致的代码,而代码存根中的不一致性经常会导致代码缺陷
auto upw1(std::make_unique<Widget>());
std::unique_ptr<Widget> upw2(new Widget); // 重复
auto spw1(std::make_shared<Widget>());
std::shared_ptr<Widget> spw2(new Widget) // 重复
异常安全:
void processWidget(std::shared_ptr<Widget> spw, int priority); // 使用new运算符而非std::make_shared processWidget(std::shared_ptr<Widget>(new Widget), computePriority()); // 存在潜藏的资源泄漏问题 // 在运行期,传递给函数的实参必须在函数调用被发起前完成评估求值,因此在调用processWidget过程中,必须执行 // 1.表达式"new Widget"必须先完成评估求值,即一个Widget对象必须现在堆上创建 // 2.由new产生的裸指针的托管对象std::shared_ptr<Widget>的构造函数必须执行 // 3.computePriority必须执行 // 编译器不必按上述顺序生成代码,编译器可能会产生如下时序操作: // 1.new Widget // 2.执行computePriority // 3.运行std::shared_ptr构造函数 // 若按上述执行,且在运行期computePriority产生异常,那么由第一步动态分配的Widget会被泄漏,因为它将永远不会被存储到在第三步才接管的std::shared_ptr中去 // 使用std::make_shared可以避免上述问题 processWidget(std::make_shared<Widget>(), computePriority()); // 在运行期std::make_shared和computePriority中肯定有一个会首先被调用。若std::make_shared首先被调用,指涉到动态分配的Widget的裸指针会在computePriority被调用前被安全存储在返回的std::shared_ptr对象中;若随后computePriority产生异常,那么std::shared_ptr的析构函数也能够知道它所拥有的Widget已被析构。若computePriority先被调用并产生异常,std::make_shared将不会被调用,因此也无须为动态分配的Widget担心 // 上述示例以及分析在std::unique_ptr上同样适用
性能提升:make系列函数会让编译器有机会利用更简洁的数据结构,产生更小更快的代码
std::shared_ptr<Widget> spw(new Widget); // 这段代码会引发两次内存分配。一次是new Widget,另一次是std::shared_ptr的构造函数中相关联的控制块
auto spw(std::make_shared<Widget>()); // 这段代码只进行了一次内存分配。只分配了单块内存既保存Widget对象又保存与其相关联的控制块。这种优化减小了程序的静态尺寸,因为代码只包含一次内存分配调用,同时还增加了可执行代码的运行速度,因为内存是一次性分配出来的。另外,还能避免控制块中的一些薄记信息的必要性,潜在减少了程序的内存痕迹总量。
// 上述示例以及分析在std::allocate_shared同样适用
不适用make系列函数的场景包括需要定制析构器,以及期望直接传递{}初始值
需要定制析构器的场景需使用new表达式
std::unique_ptr<Widget, decltype(WidgetDeleter)> upw(new Widget, WidgetDeleter);
std::shared_ptr<Widget> spw(new Widget, widgetDeleter);
make系列函数会对()内的参数进行完美转发,但无法对{}内的值进行完美转发
auto upv(std::make_shared<std::vector<int>>(10, 20)); // 指涉到包含10个元素,每个元素值为20的std::vector
// 可使用下面方式进行调用make系列函数
auto initList{10, 20};
auto spv(std::make_shared<std::vector<int>>(initList));
对于std::shared_ptr,不建议使用make系列函数的额外场景包括:1.自定义内存管理的类;2.内存紧张的系统、非常大的对象以及存在比指向到相同对象的std::shared_ptr生存期更久的std::weak_ptr
Pimpl习惯用法,即pointer to implementation,指涉到实现的指针
这种技巧就是把类的数据成员用一个指涉到某实现类(或结构体)的指针替代,之后把原来在主类中的数据成员放置到实现类中,并通过指针间接访问这些数据成员
class Widget
{
public:
Widget();
...
private:
std::string name; // 必须#include <string>
std::vector<double> data; // 必须#include <vecotr>
Gadget g1, g2, g3; // 必须#include "gadget.h"
};
// include上述头文件,增加了编译时间
Pimpl惯用法通过降低类的客户以及类实现者之间的依赖性,减少了构建(编译)次数
// widget.h // 使用Pimpl方法,将数据成员定义于某类或结构体,并且将其放于另一个头文件中,此时本文件仅需调用一次定义头文件即可,能够加快编译时间 class Widget { public: Widget(); ~Widget(); private: struct Impl; Impl* pImpl; }; // widget.cpp #include "widget.h" #include "gadget.h" #include <string> #include <vector> // Widget::Impl的实现,包括此前在Widget中的数据成员 struct Widget::Impl { std::string name; std::vector<double> data; Gadget g1, g2, g3; }; Widget::Widget() : pImpl(new Impl) { } Widget::~Wiget() { delete pImpl; } // 优化为C++14形式 class Widget { public: Widget(); ... private: struct Impl; std::unique_ptr<Impl> pImpl; }; // 结构体定义同之前 Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
对于采用std::unique_ptr来实现的pImpl指针,必须在类的头文件中声明特别成员函数,且在实现文件中定义它们。即使默认函数实现有着正确行为,也必须这样做
上述建议仅适用于std::unique_ptr,不适用于std::shared_ptr
std::move实施的是无条件的向右值类型的强制类型转换。就其本身而言,不会执行移动操作。
// C++11 std::move示例实现
template<typename T>
typename remove_reference<T>::type&& move(T&& param)
{
using ReturnType = typename remove_reference<T>::type&&;
return static_cast<ReturnType>(param);
}
// C++14
template<typename T>
decltype(auto) move(T&& param)
{
using ReturnType = remove_reference_t<T>&&;
return static_cast<ReturnType>(param);
}
仅当传入的实参被绑定到右值时,std::forward才针对该实参实施向右值类型的强制类型转换。
// 典型使用
void process(const Widget& lvalArg);
void process(Widget&& rvalArg);
template<typename T>
void logAndProcess(T&& param)
{
auto now = std::chrono::system_clock::now();
makeLogEntry("Calling 'process'", now);
process(std::forward<T>(param)); // process依据param是左值或是右值选择重载版本进行重载
}
// 使用
Widget w;
logAndProcess(w); // 调用时传入左值
logAndProcess(std::move(w)); // 调用时传入右值
// 所有函数皆为左值,因此以上对process的调用都会取用左值类型的重载版本
在运行期,std:::move和std::forward都不会做任何操作。
void f(Widget&& param); // 右值引用
Widget&& var1 = Widget(); // 右值引用
auto&& var2 = var1; // 非右值引用,万能引用
template<typename T>
void f(std::vector<T>&& param); // 右值引用
template<typename T>
void f(T&& param); // 非右值引用,万能引用
若函数模板形参具备T&&类型,并且T的类型是推导而来的,或如果对象使用auto&&声明其类型,则该形参或对象就是个万能引用。
简单来说,不涉及类型推导的T&&,就是万能引用
若声明带有const关键字且涉及类型推导的T&&,不会是万能引用,而会是右值引用
位于模板内,并不能保证涉及类型推导
// C++标准 template<class T, class Allocator = allocator<T>> class vector { public: void push_back(T&& x); ... }; // std::vector push_back不涉及类型推导 std::vector<Widget> v; class vector<Widget, allocator<Widget>> { public: void push_back(Widget&& x); // 右值引用 template<class... Args> void emplace_back(Args&&... args); // 涉及类型推导,因此是万能引用 ... };
若类型声明并不精确地具备type&&的形式,或者类型推导并未发生,则type&&就代表右值引用。
若采用右值来初始化万能引用,就会得到一个右值引用。若采用左值来初始化万能引用,就会得到一个左值引用。
针对右值引用的最后一次使用实施std::move,针对万能引用的最后一次使用实施std::forward
为了保证在完成对实参对象操作前实参不被移动,就需要保证尽在最后一次使用移动操作
在确定移动操作必不会抛出异常时,使用std::move_if_noexcept替代
针对右值引用实施std::forward,代码啰嗦,易错,且不符合习惯用法
针对万能引用实施std::move,某些左值会遭到以外改动
class Widget
{
public:
template<typename T>
void setName(T&& newName) { name = std::move(newName); }
private:
std::string name;
std::shared_ptr<SomeDataStructure> p;
};
std::string getWidgetName(); // 工厂函数
Widget w;
auto n(getWidgetName());
w.setName(n); // 此处将n值移入w,n值无条件转换为右值,调用完setName后,n值变得未知
...
作为按值返回的函数的右值引用和万能引用,依上一条所属采取相同行为。
若局部对象可能适用于返回值优化(RVO),则请勿针对其实施std::move或std::forward。
Widget makeWidget()
{
Widget w;
...
return w;
}
// 通过将拷贝转换为移动进行优化
Widget makeWidget()
{
Widget w;
return std::move(w);
}
把万能引用作为重载候选类型,几乎总会让该重载版本在始料未及的情况下被调用到。
std::multiset<std::string> names; void logAndAdd(const std::string& name) { auto now(std::chrono::system_clock::now()); log(now, "logAndAdd"); names.emplace(name); // 此处name为左值,无法避免拷贝操作 } std::string petName("Darla"); logAndAdd(petName); // 传递左值std::string,在内部需要进行一次拷贝操作 logAndAdd(std::string("Persephone")); // 传递右值std::string,因为此处实参为右值,被绑定到左值name上,原则上是可以进行移动操作,但此处进行了一次拷贝操作,事实上仅需要付出一次移动的成本 logAndAdd("Patty Dog"); // 传递字符串字面量,在传入时,因为是字符串字面量,所以隐式构造了std::string对象,但此处实际上连一次移动的成本都不需要付出 // 为了解决第二个及第三个调用语句效率低下的问题,只需要改写为万能引用 template<typename T> void logAndAdd(T&& name) { auto now(std::chrono::system_clock::now()); log(now, "logAndAdd"); names.emplace(std::forward<T>(name)); } std::string petName("Darla"); logAndAdd(petName); // 同前,进行了一次拷贝操作 logAndAdd(std::string("Persephone")); // 对右值实施了移动操作 logAndAdd("Patty Dog"); // 在multiset中直接构造一个std::string对象,而非拷贝 // 用户提供了关于索引的重载版本,此处容易发生问题 std::string nameFromIdx(int idx); void logAndAdd(int idx) { auto now(std::chrono::system_clock::now()); log(now, "logAndAdd"); names.emplace(nameFromIdx(idx)); } logAndAdd(22); // 此处调用了形参类型为int的重载版本,及用户提供的版本 // 但形参类型若为short,则会先进行隐式类型提升后进行类型精确匹配,调用万能引用版本,在emplace中调用std::string的构造函数,但其并没有short类型的构造版本,因此会发生错误
完美转发 构造函数的问题尤为严重,因为对于非常量左值类型而言,它们一般都会形成相对于拷贝构造函数的更佳匹配,并且会劫持派生类中对基类的拷贝和移动构造函数的调用。
class Person { public: template<typename T> explicit Person<T&& n> : name(std::forward<T>(n)) {} explicit Person(int idx) : name(nameFromIdx(idx)) {} // 此处会编译器会隐式生成拷贝构造函数和移动构造函数 // Person(const Person& rhs); // Person(Person&& rhs); private: std::string name; }; Person p("Nancy"); auto cloneOfP(p); // 从p出发构建新的Person类型对象,无法通过编译!此处会调用完美转发版本而非拷贝构造版本,若需要调用拷贝构造版本,则需要对p的声明添加const关键字,因为欲拷贝的对象是一个常量,所以就形成了对拷贝构造函数形参的精确匹配
若不使用万能引用和重载的组合,则替代方案包括使用彼此不同的函数名字、传递const T&类型的形参、传值和标签分派。
舍弃重载:使用彼此不同的函数名字,缺点:需要增加不必要的维护,增加代码繁琐程度
传递const T&类型的形参:缺点是必须有一次拷贝操作的成本
传值:仅在个别情况下,拷贝成本小于移动成本时,可考虑
标签分派
template<typename T> void logAndAdd(T&& name) { // logAndAddImpl(std::forward<T>(name), std::is_integral<T>()); // 不完全正确,若被推导为int&,则此处is_integral将判断为false // 应改为 logAndAddImpl(std::forward<T>(name), std::integral<typename std::remove_reference<T>::type>()); } // 需要提供std::false_type及std::true_type版本模板重载对象 template<typename T> void logAndAddImpl(T&& name, std::false_type) // 此处std::false_type为编译期值 { auto now(std::chrono::system_clock::now()); log(now, "logAndAdd"); names.emplace(std::forward<T>(name)); } std::string nameFromIdx(int idx); void logAndAddImpl(int idx, std::true_type) { logAndAdd(nameFromIdx(idx)); }
经由std::enable_if对模板施加限制,就可以将万能引用和重载一起使用,不过这种技术控制了编译器可以调用到接受万能引用的重载版本的条件。
std::enable可以强制编译器表现出来的行为如同特定的模板不存在
// 此处用于避免除Person类型外的对象传入,调用模板构造函数,产生错误
class Person
{
public:
template<typename T, typename = typename std::enable_if<!std::is_same<Person, typename std::decay<T>::type>::value>::type>
explicit Person(T&& n);
};
// 此处std::is_same用于判断类型是否一致,std::decay用于将类型值中的const及volatile修饰省略,std::enable_if::type返回的两种结果分别为std::true_type或std::false_type
派生类对象作为形参传递给基类形参时,由于派生类和基类类型不一,若存在万能引用形参构造函数,则会被优先调用,为解决这个问题,则需要通过std::is_base_of作为模板类型进行判断
class Person
{
public:
template<typename T, typename = typename std::enable_if<!std::is_base_of<Person, typename std::decay<T>::type>::value>::type>
explicit Person(T&& n);
};
// C++14中可省略typename
万能引用形参通常在性能方面具备优势,但在易用性方面一般会有劣势。
完美转发的效率高,能够避免在传递形参时创造临时对象,但存在失效的情况
可在模板函数的实现中增加static_assert用于在编译期提示是否能够进行模板实例化,若无法进行,则编译报错
class Person
{
public:
template<typename T, typename = typename std::enable_if<!std::is_base_of<Person, typename std::decay<T>::type>::value>::type>
explicit Person(T&& n)
: name(std::forward<T>(n))
{
static_assert(std::is_constructible<std::string, T>::value, "Parameter n can't be used to construct a std::string"); // 断言可以从T类型的对象构造一个std::string类型对象
}
};
完美转发的失败情形,是源于模板类型推导失败或推导结果是错误的类型。
会导致完美转发失败的实参种类有**{}、以0或NULL表达的空指针、仅有声明的整型static const成员变量、模板或重载的函数名字,以及位域**。
{}:此种初始化方式在万能引用函数模板推导类型且不在非推导语境时,会将其推导为初始化列表std::initializer_list,之后若将此传入(完美转发)形参为std::vector的函数中,则会出现错误。解决:先声明后传入
0或NULL用作空指针:此种传递类型推导结果将会是整型。解决:使用nullptr
仅有声明的整型static const成员变量:解决:增加定义即可
class Widget
{
public:
static const std::size_t MinVals = 28;
...
};
std::vector<int> widgetData;
widgetData.reserve(Widget::MinVals);
重载的函数名字和模板名字:解决:手动指定需要转发的那个重载版本或实例
void f(int (*pf)(int)); void f(int pf(int)); int processVal(int value); int processVal(int value, int priority); f(processVal); // 没问题 fwd(processVal); // 错误,无法得知processVal的重载版本,编译器重载决议失败 template<typename T> T workOnVal(T param) { ... } fwd(processVal); // 错误,无法得知需要workOnVal的模板实例版本 using ProcessFuncType = int (*)(int); ProcessFuncType processValPtr = processVal; fwd(processValPtr); // 没问题 fwd(static_cast<ProcessFuncType>(workOnVal)); // 没问题
位域:位域由机器字的若干任意部分组成的,这样的实体不可能有办法对其直接取址。在硬件层次,指针和引用属于同一事物,因此无法创建指涉到任意比特的指针(C++可以指涉的最小实体是单个char),自然无法引用绑定到任意比特。能够传递位域值的仅有的形参种类只有按值传递以及常量引用。解决:将位域值转换为普通类型副本,之后转发
struct IPv4Header
{
std::uint32_t version:4,
IHL:4,
DSCP:6,
ECN:2,
totalLength:16;
...
};
auto length(static_cast<std::uint16_t>(h.totalLength));
fwd(length);
按引用的默认捕获会导致空悬指针问题。
using FilterContainer = std::vector<std::function<bool(int)>>; FilterContainer filters; // 在运行期内动态计算除数 void addDivisorFilter() { auto cal1 = computeSomeValue1(); auto cal2 = computeSomeValue2(); auto divisor = computeDivisor(cal1, cal2); // 这一步对divisor的指涉可能空悬。因为在addDivisorFilter函数返回时,divisor就不再存在,此时可能刚好添加至容器内 filters.emplace_back([&](int value) { return value % divisor == 0; }); // 按值捕获是解决此问题的一种方法,但按值捕获只能捕获非静态局部变量 filters.emplace_back([=](int value) { return value % divisor == 0; }); }
按值的默认捕获极易受空悬指针影响(尤其是this),并会误导人们认为lambda式是独立的。
class Widget { public: ... void addFilter() const; private: int divisor; }; // 此处divisor并非非静态局部变量,而是类成员变量,无法被捕获 void Widget::addFilter() const { filters.emplace_back([=](int value){ return value % divisor == 0; }); } // 此处代码等同于,因为每个非静态成员函数都持有一个this指针,然后每当调用该类的成员变量时都会用到这个指针 void Widget::addFilter() const { auto currentObjectPtr = this; filters.emplace_back([currentObjectPtr](int value) { return value % currentObjectPtr->divisor == 0; }); }
使用智能指针
void doSomeWork() { auto pw = std::make_unique<Widget>(); pw->addFilter(); } // 在此函数返回后,filters将持有空悬指针 // 解决 void Widget::addFilter() const { auto divisorCopy = divisor; filters.emplace_back([divisorCopy](int value){ return value % divisorCopy == 0; }); } // C++14中支持广义lambda捕获 // 此处将divisor拷贝入闭包,使用副本进行操作 void Widget::addFilter() const { filters.emplace_back([divisor = divisor](int value){ return value % divisor == 0; }); }
使用C++14的初始化捕获将对象移入闭包。
在C++11中,经由手工实现的类或std::bind去模拟初始化捕获。
// 手工实现类 class IsValAndArch { public: using DataType = std::unique_ptr<Widget>; explicit IsValAndArch(DataType&& ptr) : pw(std::move(ptr)) {} bool operator()() const { return pw->isValidated() && pw->isArchived(); } private: DataType pw; }; auto func = IsValAndArch(std::make_unique<Widget>());
// std::bind模拟初始化捕获 // C++14 std::vector<double> data; auto func = [data = std::move(data)] { }; // C++11 auto func = std::bind([](const std::vector<double>& data) { }, std::move(data)); // 绑定对象含有传递给std::bind所有实参的副本。对于每个左值实参,在绑定对象内的对应的对象内对其实施的是拷贝构造;而对于每个右值实参,实施的则是移动构造。data在绑定对象中实施的是移动构造,而该移动构造动作正是实现模拟移动捕获的核心所在,因为把右值移入绑定对象,正式绕过C++11无法将右值移入闭包的手法 // 当一个绑定对象被调用时,它所存储的实参会传递给原先传递给std::bind的那个可调用对象,即data的副本作为实参传递给lambda式 // 针对const operator()() auto func = std::bind([](std::vector<double>& data) mutable {}, std::move(data));
C++14特性,lambda可以在形参规则中使用auto
auto f = [](auto x) { return func(normalize(x)); }; // 闭包类的函数调用运算符 class SomeCompilerGeneratedClassName { public: template<typename T> auto operator()(T x) const { return func(normalize(x)); } }; // 若normalize区别对待左值和右值,则该lambda式撰写存在问题,需要将其改写为完美转发形式 auto f = [](auto&& x) { return func(normalize(std::forward<???>(x))); }; // 此处类型无法确认。但根据条款28,若把左值传递给万能引用的形参,则该形参的类型会成为左值引用,若为右值,则同理。传入decltype(x),若x绑定了左值,则将产生左值引用类型,符合惯例;反之将产生右值引用惯例,而非符合惯例的非引用,虽然如此,但产生的结果殊途同归 auto f = [](auto&& param) { return func(normalize(std::forward<decltype(param)>(param))); }; // C++14lambda式能够接受可变长形参,改进如下 auto f = [](auto&&... params) { return func(normalize(std::forward<decltype(params)>(params)...)); }; // std::forward C++14实现 template<typename T> T&& forward(remove_reference_t<T>& param) { return static_cast<T&&>(param); } // 左值结果已知;若完美转发右值,则会发生引用折叠将Widget&& && 变为Widget&& Widget&& forward(Widget& param) { return static_cast<Widget&&>(param); }
std::thread未提供直接获取异步运行函数返回值的途径,而且若那些函数抛出异常,程序就会终止。
doAsyncWork以异步方式运行,有两种方式:
std::thread
int doAsyncWork();
std::thread t(doAsyncWork);
std::async
auto fut = std::async(doAsyncWork); // 此处auto因推导为std::future,易于捕获异常
基于线程的程序设计要求手动管理线程耗尽、超订、负载均衡,以及新平台适配。
基于线程和基于任务的区别在于,居于任务的程序设计表现为更高阶的抽象
线程在带有并发C++软件中的三种意义
线程耗尽:软件线程是一种有限的资源,若试图传教的线程数量多余系统能够提供的数量,则无论情况如何(即便声明了关键字noexcept)会抛出std::system_error异常
int doAsyncWork() noexcept;
std::thread t(doAsyncWork); // 若无可用线程,则抛出异常
超订问题:就绪状态的软件线程超过了硬件线程数量的时候
经由应用了默认启动策略的std::async进行基于任务的程序设计,大部分这类问题都能找到解决之道。
std::async的默认启动策略既允许任务以异步方式执行,也允许任务以同步方式执行。
有两种标准策略,它们都是用限定作用域的枚举类型std::launch中的枚举量来表示的:
若未指定启动策略,则采用默认策略,即上述两种策略的或运算的结果,以异步或同步的方式运行皆可。使得std::async与标准库的线程管理组件能够承担得起线程的创建和销毁、避免超订以及负载均衡的责任。但这种方法会存在一些未知性
// 以下两种写法具有相同的意义
auto fut1(std::async(f));
auto fut2(std::async(std::launch::async | std::launch::deferred, f));
如此弹性会导致使用thread_local变量时的不确定性,隐含着任务可能永远不会执行,还会影响运行了基于超时的wait调用的程序逻辑。
这意味着若f读或写此线程级局部存储(thread-local storage,TLS)时,无法预知会取到的是哪个线程的局部存储
对任务调用wait_for或wait_until会产生std::launch::deferred一值
// 循环至f完成运行,但可能永远不会完成 using namespace std::literals; void f() { std::this_thread::sleep_for(1s); } auto fut(std::async(f)); while(fut.wait_for(100ms)) != std::future_status::ready) { ... } // 若f与调用std::async的线程是并发执行的,不会发生异常;若f被推迟执行,则fut.wait_for将总返回std::future_status::deferred,而那永远也不会取值std::future_status::ready,所以循环也就永远不会终止 // 优化:判断std::async返回的期值,确定任务是否被推迟,若确实被推迟,则避免进入基于超时的循环 auto fut(std::async(f)); if(fut.wait_for(0s) == std::future_status::deferred) { // 此处使用fut的wait或get以异步方式调用f ... } else { while(fut.wait_for(100ms) != std::future_status::ready) { // 任务既未被推迟,也未就绪,则继续并发工作,直至任务就绪 ... } // fut就绪 ... }
以默认启动策略对任务使用std::async能正常工作需要满足以下所有条件,只要其中一个条件不满足,就很有可能要确保任务以异步方式执行:
若异步是必要的,则指定std::launch::async。
auto fut(std::async(std::launch::async, f));
// 以下函数能像std::async那样运作,且会自动地使用std::launch::async作为启动策略
// 该函数接受一个可调用对象f,以及零个或多个形参params,并将后者完美转发给std::async,同时传递std::launch::async作为启动策略
// C++11版本
template<typename F, typename... Ts>
inline std::future<typename std::result_of<F(Ts...)>::type> reallyAsync(F&& f, Ts&&... params)
{
return std::async(std::launch::async, std::forward<F>(f), std::forward<Ts>(params)...);
}
// C++14版本
template<typename F, typename... Ts>
inline auto reallyAsync(F&& f, Ts&&... params)
{
return std::async(std::launch::async, std::forward<F>(f), std::forward<Ts>(params)...);
}
使std::thread类型对象在所有路径皆不可联结(join)。
在析构时调用join可能导致难以调试的性能异常。
含有此行为的对象称为RAII对象,来自RAII类(即资源获取即初始化),关键在于析构而非初始化
RAII类在标准库中很常见,如STL容器、标准智能指针、std::fstream类型对象等
没有std::thread类型对象对应的RAII类,可自行实现,如下:
class ThreadRAII { public: enum class DtorAction { join, detach }; ThreadRAII(std::thread&& t, DtorAction a) : action(a), t(std::move(t)) { } ~ThreadRAII() { if(t.joinable()) { if(action == DtorAction::join) { t.join(); } else { t.detach(); } } } std::thread& get() { return t; } private: DtorAction action; std::thread t; };
在析构时调用detach可能导致难以调试的未定义行为。
在成员列表的最后声明std::thread类型对象。
期值的析构函数在常规情况下,仅会析构期值的成员变量。
指涉到经由std::async启动的未推迟任务的共享状态的最后一个期值会保持阻塞,直至该任务结束。
// 该容器的析构函数可能会在其析构函数中阻塞,因为它所持有的期值中可能会有一个或多个指涉到经由std::async启动未推迟任务所产生的共享状态 std::vector<std::future<void>> futs; // Widget对象可能会在其析构函数中阻塞 class Widget { public: ... private: std::shared_future<double> fut; }; // 判定给定的期值不满足触发特殊析构行为的条件,std::packaged_task,此类型对象会准备一个函数(或其他可调用对象)以供异步执行,手法是将它奖赏一层包装,将其结果置入共享状态,而指涉到该共享状态的期值则可以经由std::packaged_task的get_future函数得到 { int calcValue(); std::packaged_task<int()> pt(calcValue); // 创建后便以异步方式开始执行 // 该期值对象fut未指涉到由std::async调用产生的共享状态,因此它的析构函数将表现出常规行为 auto fut = pt.get_future(); std::thread t(std::move(pt)); // 控制句柄。std::packaged_task对象不可拷贝,因此在此处需要将其强制转型为右值 ... // ...中对t做处理的三种可能: // 1.未对t实施任何操作。由于此时t在作用域结束前可联结,将导致程序终止 // 2.针对t实施join。调用后由于t变为不可联结,此时fut无须在析构函数中阻塞 // 3.针对t实施detach。此时fut无须在析构函数中实施detach }
// 条件变量使用,在多线程中需要搭配互斥量使用
std::condition_variable cv; // 事件条件变量
std::mutex m;
// 执行线程
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, [] { return; }); // wait可搭配测试等待条件的lambda式或其他函数对象
// 另一个线程,检测事件,通知反应任务
...
cv.notify_one(); // 通知一个反应任务
cv.notify_all(); // 通知多个反应任务
若仅为了实现平凡事件通信,基于条件变量的设计会要求多余的互斥量,这会给相互关联的检测和反应任务带来约束,并要求反应任务校验事件确已发生。
使用标志位的设计可以避免上述问题,但这一设计基于轮询而非阻塞。
// 共享布尔标志位
std::atomic<bool> flag(false);
...
// 检测事件
flag = true;
// 反应任务轮询标志位
while(!flag); // 等待事件
... // 针对事件作出反应
条件变量和标志位可以一起使用,但这样的通信机制设计结果不甚自然。
std::condition_variable cv; std::mutex m; bool flag(false); // 检测任务 ... // 检测事件 { std::lock_guard<std::mutex> g(m); flag = true; } cv.notify_one(); // 反应任务 ... // 准备反应 { std::unique_ptr<std::mutex> lk(m); cv.wait(lk, [] { return flag; }); // 使用lambda式应对虚假唤醒 ... // 针对事件作出反应(m被锁定) } ... // 继续等待反应(m已解锁) // 以上仍然需要在通知条件变量才能让反应任务被唤醒去检测标志位,不够干净利落
使用std::promise类型对象和期值(std::future或std::shared_future)就可以回避这些问题,但是第一:这个途径为了共享状态需要使用堆内存;第二:仅限于一次性通信。
std::promise<void> p;
// 检测任务
...
p.set_value();
// 反应任务
...
p.get_future().wait();
...
std::promise和期值之间是共享状态,而共享状态通常是动态分配的,因此就必须考虑在堆上进行分配和回收的成本
std::promise类型对象只能设置一次。
// 假设只暂停线程一次,则使用void期值的设计即为合理 std::promise<void> p; void react(); // 反应任务函数 void detect() { std::thread t([] { p.get_value().wait(); // 暂停线程t直至期值被设置 react(); }); ... p.set_value(); // 取消暂停t(调用react) ... t.join(); // 使t处于不可联结状态 } // detect使用RAII方法 void detect() { ThreadRAII tr(std::thread([] { p.get_future().wait(); react(); })), ThreadRAII::DtorAction::join); // 此处存在风险 ... // 此处若抛出异常,程序中止,react将永远不会被执行,lambda式将永远不能完成,函数失去响应,因为tr的析构函数永远无法执行 p.set_value(); ... } // 将std::future换为std::shared_future,使得每个反应线程都需要自己的那份std::shared_future副本去指涉到共享状态 std::promise<void> p; void detect() { auto sf = p.get_value().share(); std::vector<std::thread> vt; // 反应任务池 for(int i = 0; i < threadsToRun; ++i) { vt.emplace_back([sf]{ st.wait(); react(); }); } ... // 若此处抛出异常,则detect会失去响应 p.set_value(); // 使所有线程取消暂停 ... for(auto& t : vt) { t.join(); } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。