Effective Modern C++

模板类型推导

template<typename T>
void f(T& parms);//reference
template<typename T>
void f(const T& parms);//const ref
template<typename T>
void f(T* parms);//pointer
template<typename T>
void f(T&& parms);//universal reference
template<typename T>
void f(T parms);//value

注意 char[] 与char* 当按值传递时,模板推导均为char*
但按照引用传递时 模板推导为 char[]

auto

可以认为是一种运行时模板,c++11 函数返回值类型为auto 需增加 ->指出返回类型
c++14 之后则不需要

//c++11
template<typename T,typename B>
auto ff(T& t,B b)->decltype(t[b]);
//c++14
template<typename T,typename B>
decltype(auto) ff(T& t,B b);

auto 声明变量必须有初值,可以使用{}或者()进行初始化

auto bb = {
    
    1,2,3}
// deduce std::initializer_list<int>

利用auto 进行类型推导,避免不同环境不同版本的类型不统一

vector<bool>

在这里插入图片描述
static_cast<T> 明确指定返回类型
底层对于bool进行存储优化01 存储

decltype

decltype(( )) 认为是该类型的引用
利用 typeid(x).name() 进行查看该推导类型

using boost::typeindex::type_id_with_cvr;
type_id_with_cvr<T>().pretty_name();

或者使用cppinsights.io

对象初始化{} vs ()

利用{},()均可以初始化
对于类来说()更倾向于认为为函数而不是类初始化
此外{} 更倾向于转换成std::initializer_list<T>
且这种初始化不支持narrow conversation

对于vector ()初始化是指定个数和初值,避免歧义

优先选择nullptr,NULL 在一些编译器中会转化成整数0

using and typedef

using 可以利用模板进行定义,而typedef 需要将其包裹在类或者结构体当中
在这里插入图片描述

enum class 与 enum

enum Color {
    
    black,white,red);
enum class Color{
    
    black,white,red);

作用域,enum 全局变量,不能再次定义
enum class 作用域只在{} 之内
而且进行比较时enum class 需要进行显式转换

override and final

override
派生类所需要求:
在这里插入图片描述
显示指定override ,编译器会进行检查
在这里插入图片描述
final
指定不进行继承

constexpr

constexpr需要初始化不可运行时赋值
c++14之前constexpr 函数中不可以出现for循环,对于编译器计算过于复杂
c++ 14 之后可以使用for循环再 函数当中

内联变量

在这里插入图片描述
&Bar::number 会报错,堆中无明确地址,不可用ref
在这里插入图片描述

const 成员函数线程安全

在这里插入图片描述
利用mutable 修改const 函数

特种成员函数的生成机制

构造函数
析构函数
拷贝构造函数
拷贝赋值函数
移动构造函数
移动赋值函数
在这里插入图片描述
在这里插入图片描述

PImpl 用法

pointer to implementation

在这里插入图片描述
unique需要知道空间大小才能够析构
在这里插入图片描述
shared ptr 运行时确认

完美转发

std::move 将左值引用转换为右值引用
std::forward 左值返回左值,右值返回右值
std::move与std::forward本质都是static_cast转换,对于右值引用使用std::move,对于万能引用使用std::forward。

右值引用

万能引用必然在模板函数中使用,否则就没有意义

在 C++11 之前,返回一个本地对象意味着这个对象会被拷贝,除非编译器发现可以做返回值优化(named return value optimization,或 NRVO),能把对象直接构造到调用者的栈上。从 C++11 开始,返回值优化仍可以发生,但在没有返回值优化的情况下,编译器将试图把本地对象移动出去,而不是拷贝出去。这一行为不需要程序员手工用 std::move 进行干预——使用std::move 对于移动行为没有帮助,反而会影响返回值优化。

引用折叠

引用折叠只有两条规则:

一个 rvalue reference to an rvalue reference 会变成 (“折叠为”) 一个 rvalue reference.
所有其他种类的"引用的引用" (i.e., 组合当中含有lvalue reference) 都会折叠为 lvalue reference.

万能引用重载

Rvalue references只能绑定到右值上,lvalue references除了可以绑定到左值上,在某些条件下还可以绑定到右值上。[1] 这里某些条件绑定右值为:常左值引用绑定到右值,非常左值引用不可绑定到右值!

当一个universal reference开始被lvalue初始化的时候,var2就变成了lvalue reference。

lambda避免默认捕获模式

在这里插入图片描述
这里匿名函数会捕获Bar* this,&val_local,&val_localParam ,而静态变量,全局变量有指定的存储空间并不保存在栈当中,不会被捕获地址
引用传递方式(相当于编译器自动为我们按引用传递了所有局部变量),是const引用。

[=] 按值捕获
匿名函数在编译器当中会被解析为一个类,捕获的对象为类中私有变量,并重载操作符()

按引用捕获注意局部变量可能已经被释放,访问为空地址

捕获类的变量,可以定义局部变量进行赋值再捕获赋值的变量

lambda 与 bind

auto newCallable = bind(callable,arg_list);


int f(int a, int b, int c) {
    
    
    cout << "a " << a << " b " << b << " c " << c << endl;
    return a - b + c;
}
//调用处
auto g = bind(f, _2, _1, 11);//注意:此处参数顺序.._2,_1),但是bind函数参数顺序是(20,10)
int k  = g(10, 20);
cout << k << endl;  //输出21

C++11里,有一些情况下,只能使用std::bind,不可以使用lambda表达式
从C++14起,任何std::bind都可以用lambda表达式来代替,因为泛型Lambda的出现让Lambda开始支持polymorphic
关于第一点,有这么一些区别:

C++11里的lambda表达式,其capture list里只能捕获lvalues,但std::bind可以使用右值,比如auto f1 = std::bind(f, 42, _1, std::move(v));
Expressions can’t be captured, only identifiers can,而std::bind可以写:auto f1 = std::bind(f, 42, _1, a + b);
std::bind支持Overloading arguments for function objects
lambda表达式Impossible to perfect-forward arguments

std::function

std::function就是调用对象的封装器,可以把std::function看做一个函数对象,用于表示函数这个抽象概念。std::function的实例可以存储、复制和调用任何可调用对象,存储的可调用对象称为std::function的目标,若std::function不含目标,则称它为空,调用空的std::function的目标会抛出std::bad_function_call异常。

std::function<void(int)> f; // 这里表示function的对象f的参数是int,返回值是void
#include <functional>
#include <iostream>

struct 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::function<void(int)> f_display = print_num;
    f_display(-9);

    // 存储 lambda
    std::function<void()> f_display_42 = []() {
    
     print_num(42); };
    f_display_42();

    // 存储到 std::bind 调用的结果
    std::function<void()> f_display_31337 = std::bind(print_num, 31337);
    f_display_31337();

    // 存储到成员函数的调用
    std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;
    const Foo foo(314159);
    f_add_display(foo, 1);
    f_add_display(314159, 1);

    // 存储到数据成员访问器的调用
    std::function<int(Foo const&)> f_num = &Foo::num_;
    std::cout << "num_: " << f_num(foo) << '\n';

    // 存储到成员函数及对象的调用
    using std::placeholders::_1;
    std::function<void(int)> f_add_display2 = std::bind(&Foo::print_add, foo, _1);
    f_add_display2(2);

    // 存储到成员函数和对象指针的调用
    std::function<void(int)> f_add_display3 = std::bind(&Foo::print_add, &foo, _1);
    f_add_display3(3);

    // 存储到函数对象的调用
    std::function<void(int)> f_display_obj = PrintNum();
    f_display_obj(18);
}

当给std::function填入合适的参数表和返回值后,它就变成了可以容纳所有这一类调用方式的函数封装器。C++里如果需要使用回调那就一定要使用std::function

优先任务而非线程

c++并发中,线程(thread)的三种含义:

硬件线程(hardware threads): 实际执行计算的线程。现代的机器架构为每个CPU核心提供一个或多个硬件线程。
软件线程(software threads):也叫操作系统线程(OS threads)或系统线程(system threads),操作系统跨所有进程管理的线程,并在硬件线程上调度执行。通常可以创建比硬件线程更多的软件线程,因为当软件线程被阻塞时(例如,在I/O或等待互斥锁或条件变量时),通过执行其他未阻塞的线程可以提高吞吐量。
标准库中的线程std::threads: c++进程中作为底层软件线程句柄的对象。一些std::thread对象表示空句柄,也就是说,没有对应的软件线程,因为它们处于默认构造状态(因此没有函数执行),已经被移动了(移动后的std::thread作为底层软件线程的句柄),已经被joined了(要运行的函数已经完成了),或者已被detached了(与底层软件线程之间的连接已被切断)。

优先使用 async 而不是 thread
调用async 会返回一个future 对象

异步launch::async

如果异步是必要的那么指定std::launch::async

请添加图片描述
std::async的默认发射策略既允许任务异步执行,又允许任务同步执行。
这个灵活性(上一点)导致了使用thread_local变量时的不确定性,它隐含着任务可能不会执行,它还影响了基于超时的wait调用的程序逻辑。

auto fut = std::async(f);       // 如前

if (fut.wait_for(0) == std::future_status::deferred)  // 如果任务被推迟
{
    
    
    ...     // fut使用get或wait来同步调用f
} else {
    
                // 任务没有被推迟
    while(fut.wait_for(100ms) != 
         std::future_status::ready) {
    
           // 不可能无限循环(假定f会结束)

      ...    // 任务没有被推迟也没有就绪,所以做一些并发的事情直到任务就绪
    }

    ...        // fut就绪
}

保证在所有路径上thread unjoinable

  1. 线程等待:join()

(1)等待子线程结束,调用线程处于阻塞模式

(2)join()执行完成之后,底层线程id被设置为0,即joinable()变为false。同时会清理线程相关的存储部分, 这样 std::thread 对象将不再与已经底层线程有任何关联。这意味着,只能对一个线程使用一次join();调用join()后,joinable()返回false。

  1. 线程分离:detach()

(1)分离子线程,与当前线程的连接被断开,子线程成为后台线程,被C++运行时库接管。这意味着不可能再有std::thread对象能引用到子线程了。与join一样,detach也只能调用一次,当detach以后其joinable()为false。

(2)注意事项:

①如果不等待线程,就必须保证线程结束之前,可访问的数据是有效的。特别是要注意线程函数是否还持有一些局部变量的指针或引用。

②为防止上述的悬空指针和悬引用的问题,线程对象的生命期应尽量长于底层线程的生命期

(3)应用场合

①适合长时间运行的任务,如后台监视文件系统、对缓存进行清理、对数据结构进行优化等。

②线程被用于“发送即不管”(fire and forget)的任务,任务完成情况线程并不关心,即安排好任务之后就不管。

一个std::thread对象只可能处于可联结或不可联结两种状态之一。可用joinable()函数来判断,即std::thread对象是否与某个有效的底层线程关联(内部通过判断线程id是否为0来实现)。

1. 可联结(joinable):当线程可运行、己运行或处于阻塞时是可联结的。注意,如果某个底层线程已经执行完任务,但是没有被join的话(线程池),该线程依然会被认为是一个活动的执行线程,仍然处于joinable状态。

2. 不可联结(unjoinable):

(1)当不带参构造的std::thread对象为不可联结,因为底层线程还没创建。

(2)己移动的std::thread对象为不可联结。因为该对象的底层线程id会被设置为0。

(3)己调用join或detach的对象为不可联结状态。因为调用join()以后,底层线程己结束,而detach()会把std::thread对象和对应的底层线程之间的连接断开。

条款38 对不同线程句柄的析构保持关注

future 与其对应的底层软件线程之间存在shared state来存放线程返回的结果

标准库有两个future的模板:std::future和std::shared_future。在很多情况下,这种区别并不重要,所以这里简称为future,指的是两种future。

考虑如下场景:被调用方(通常是异步运行的)将计算结果写入通信通道(通常是通过std::promise对象),调用方使用future读取结果,其中虚线箭头表示信息流:
在这里插入图片描述

被调用方的结果应该存储在哪里?

  1. 不可能存储在被调用放的std::promise中。因为在调用方在future对象调用get()之前被调用方可能已经完成了计算,而std::promise对象作为被调用方的局部变量,当调用完成的时候将会被销毁。
  2. 也不可能存储在调用方的future中。因为一个std::future可以用来创建一个std::shared_future(将被调用者返回的结果的所有权从std::future移动到了std::shared_future),std::shared_future在原始std::future被销毁后,可以被复制多次。假设不是所有的结果类型都可以被复制(例如,只能移动的类型),并且返回结果的生命周期至少应该在最后一个future引用它时还有效,此时无法判断多个future中的哪个是最后一个使用返回结果的,需要考虑如何存储返回的结果。
  3. 返回的结果被存储在shared state中。因为与被调用方关联的对象和与调用方关联的对象都不适合存储被调用方的结果,所以它被存储在两者之外的位置。这个位置称为shared state。shared state通常由基于堆的对象表示,但它的类型、接口和实现不是由标准指定的,标准库的作者可以他们喜欢的方式实现shared state。
    我们可以将被调用者、调用者和共享状态之间的关系想象成下图:

在这里插入图片描述

shared state很重要,因为一个future析构函数的行为是由与future相关的shared state决定的

正常行为的例外情况仅在某个future同时满足下列所有情况下才会出现:

  1. 它关联到由于调用std::async而创建出的共享状态。
  2. 任务的启动策略是std::launch::async,或者在对std::async的调用中指定了该策略。
  3. 这个future是关联共享状态的最后一个future。对于std::shared_future,如果还有其他的std::shared_future,与要被销毁的future引用相同的共享状态,则要被销毁的future遵循正常行为(即简单地销毁它的数据成员)。

只有当上面的三个条件都满足时,future的析构函数才会表现“异常”行为,就是在异步任务执行完之前阻塞住。实际上,这相当于对由于运行std::async创建出任务的线程隐式join。

future的正常析构行为就是销毁future本身的数据成员。
引用了共享状态——使用std::async启动的未延迟任务建立的那个——的最后一个future的析构函数会阻塞住,直到任务完成。

线程间一次性通信考虑使用future

也许更重要的是,std::promise只能设置一次。std::promise和future之间的通信是一次性的:不能重复使用。这是与基于条件变量或者基于flag的设计的明显差异,条件变量和flag都可以通信多次。(条件变量可以被重复通知,flag也可以重复清除和设置。)

现在,std::promise和futures(即std::future和std::shared_future)都是需要类型参数的模板。形参表明通过通信信道被传递的信息的类型。在这里,没有数据被传递,只需要让反应任务知道它的future已经被设置了。我们在std::promise和future模板中需要的东西是表明通信信道中没有数据被传递的一个类型。这个类型就是void。检测任务使用std::promise,反应任务使用std::future或者std::shared_future。当感兴趣的事件发生时,检测任务设置std::promise,反应任务在future上wait。尽管反应任务不从检测任务那里接收任何数据,通信信道也可以让反应任务知道,检测任务什么时候已经通过对std::promise调用set_value“写入”了void数据。

void detect()
{
    
    
    ThreadRAII tr(                      //使用RAII对象
        std::thread([]
                    {
    
    
                        p.get_future().wait();
                        react();
                    }),
        ThreadRAII::DtorAction::join    //有危险!(见下)
    );//tr中的线程在这里被挂起
    p.set_value();                      //解除挂起tr中的线程}

问题在于第一个“…”区域中(注释了“tr中的线程在这里被挂起”的那句),如果异常发生,p上的set_value永远不会调用,这意味着lambda中的wait永远不会返回。那意味着在lambda中运行的线程不会结束,这是个问题,因为RAII对象tr在析构函数中被设置为在(tr中创建的)那个线程上实行join。换句话说,如果在第一个“…”区域中发生了异常,函数挂起,因为tr的析构函数永远无法完成。

std::promise<void> p;                   //跟之前一样
void detect()                           //现在针对多个反映线程
{
    
    
    auto sf = p.get_future().share();   //sf的类型是std::shared_future<void>
    std::vector<std::thread> vt;        //反应线程容器
    for (int i = 0; i < threadsToRun; ++i) {
    
    
        vt.emplace_back([sf]{
    
     sf.wait();    //在sf的局部副本上wait;
                              react(); });  //emplace_back见条款42
    }//如果这个“…”抛出异常,detect挂起!
    p.set_value();                      //所有线程解除挂起for (auto& t : vt) {
    
                    //使所有线程不可结合;
        t.join();                       //“auto&”见条款2
    }
}

  • 对于简单的事件通信,基于条件变量的设计需要一个多余的互斥锁,对检测和反应任务的相对进度有约束,并且需要反应任务来验证事件是否已发生。
  • 基于flag的设计避免的上一条的问题,但是是基于轮询,而不是阻塞。
  • 条件变量和flag可以组合使用,但是产生的通信机制很不自然。
  • 使用std::promise和future的方案避开了这些问题,但是这个方法使用了堆内存存储共享状态,同时有只能使用一次通信的限制。

atomic and volatile

在C++中,有些编译器在实现时也将并发的某种含义加入到了volatile关键字中(但仅仅是在用那些编译器时)。因此在此值得讨论下关于volatile关键字的含义以消除异议。

开发者有时会与volatile混淆的特性——本来应该属于本章的那个特性——是std::atomic模板。这种模板的实例化(比如,std::atomic,std::atomic,std::atomic<Widget*>等)提供了一种在其他线程看来操作是原子性的的保证(译注:即某些操作是像原子一样的不可分割。)。一旦std::atomic对象被构建,在其上的操作表现得像操作是在互斥锁保护的关键区内,但是通常这些操作是使用特定的机器指令实现,这比锁的实现更高效。

  • std::atomic用于在不使用互斥锁情况下,来使变量被多个线程访问的情况。是用来编写并发程序的一个工具。
  • volatile用在读取和写入不应被优化掉的内存上。是用来处理特殊内存的一个工具。

对于移动成本低且总是被拷贝的可拷贝形参,考虑按值传递

  • 对于可拷贝,移动开销低,而且无条件被拷贝的形参,按值传递效率基本与按引用传递效率一致,而且易于实现,还生成更少的目标代码。
  • 通过构造拷贝形参可能比通过赋值拷贝形参开销大的多。
  • 按值传递会引起切片问题,所说不适合基类形参类

emplace and push

emplace 强制转换,就地创建

push 创建后拷贝

置入函数使用直接初始化,这意味着可能使用explicit的构造函数。插入函数使用拷贝初始化,所以不能用explicit的构造函数。当你使用置入函数时,请特别小心确保传递了正确的实参,因为即使是explicit的构造函数也会被编译器考虑,编译器会试图以有效方式解释你的代码。

  • 原则上,置入函数有时会比插入函数高效,并且不会更差。
  • 实际上,当以下条件满足时,置入函数更快:(1)值被构造到容器中,而不是直接赋值;(2)传入的类型与容器的元素类型不一致;(3)容器不拒绝已经存在的重复值。
  • 置入函数可能执行插入函数拒绝的类型转换。

猜你喜欢

转载自blog.csdn.net/ltd0924/article/details/130186337