Effective Modern C++阅读笔记

  • strerror和perror的区别

    char strerror(interrnum)接收一个错误码,返回错误信息
    void perror(const char
    s)先打印字符串s,再根据当前errno打印信息

目录

模板型别推导有3种情况

  • 当形参类型是指针或引用且不是万能引用时:忽略引用,不会忽略const、volatile
  • 当是万能引用时:按实参是左值还是右值来区分。左值推导 型别为左值引用,右值推导 型别为右值(非引用类型),然后发生引用折叠,左值结果为左值引用,右值结果为右值引用。
  • 既不是指针也不是引用:忽略const、忽略引用、volatile。
    const char* const:右const是指针本身不可修改,在按值传递时,指针本身被按比特复制失去常量性,而所指对象本身的常量性得到保留,即const char*
    volatile int x = 2;
    auto y = x;//y为int型别
    

实参是数组时:如果形参类别是普通的按值传递,那么数组退化为指针。如果是按引用传递,那么推导出的是数组型别。

auto的推导:与模板型别推导一样,但多了初始化列表的情况

auto表达式推导会将{}视作std::initializer_list进行推导,而模板型别推导中不会这样
在c++14中允许auto作为函数返回值,也允许在lamda中使用auto。如果是auto返回值的函数,是不能返回{…}的,因为这是使用的模板型别推导。lambda表达式也不行。

decltype:一般得出的就是该名字的声明型别 (declared type)

使用 返回值型别尾序语法 是为了 在指定返回值型别时可以使用函数形参。而在c++14中可以直接写auto来推导,不需要写成auto fun(xx) ->decltype(xx)
大部分容器的operator[]会返回T&类型,除了std::vector< bool>
对于delctype((x))会返回x类型的引用。因为对于型别为 T 的左值表达式,除非该表达式仅有一个名字, 否则decltype 总是得出型别 T&

c++14decltype(auto):先用表达式替换decltype(auto)当中的auto,再根据decltype语法规则来确定变量的类型

使用{}来正确调用构造函数

Widget w3{}优于Widget w3(),因为后者会被错误的解析成声明了一个函数

当有构造函数写了std::initializer_list<…>的参数,那么使用{}初始化时,编译器会想方设法的去优先调用那个版本,即使是进行强制类型转换。

class A{
public:
    A(int num){
        std::cout<<"int"<<std::endl;
    }
    A(std::initializer_list<double> num){
        std::cout<<"initializer_list"<<std::endl;
    }
};
int main(){
    A b{2};//initializer_list

nullptr实际上也不是指针型别,而是std::nullptr_t,该型别可以隐式类型转换为任意裸指针型别

使用别名声明using,而不是typedef

带依赖型别必须前面加个typename,也就是说使用typedef xx xx_name定义了一个带依赖型别时,使用它需要加typename,而using就不需要写

//1.使用using:支持模板化,不需要像下面这样使用::type来取得实际类型
template<class T>
using v2type = std::vector<T> ;
//2.使用typedef:它不支持模板化,必须嵌套在模板化的类里面才能使用
template<class T>
struct A
{
    typedef std::vector<T> v1type;
};

template<class T>
struct B
{
    typename A<T>::v1type v3;//带依赖类型 必须使用typename
    v2type<T> v4;//使用using得到的是非带依赖的
};

在c++11中type_traits还需要使用::type才能得到具体类型,就是因为它使用的typedef来实现的,c++14才为它们加上了别名模板,命名为std::xx_t,例如std::remove_const_t< T>
template <class l>
using remove_const_t = typename remove_const<l>::type;

使用enum class Color而非enum Color

前者是c++11提出的限定作用域的枚举型别,且可以避免隐式类型转换为整型等底层类型。
限定作用域的枚举型别 底层类型默认为int, 而非限定的型别 无默认的底层类型。但它们都可以自行指定底层类型来实现前置声明

enum class Color:std::uint32_t{
    red = 0,
    black = 1
};

使用非限定枚举类型来 记录 元组里的东西。使用非限定是为了方便的隐式类型转换

//一个元组,里面保存了员工的id和name
std::tuple<int, std::string> a{2,"yx"};
//使用一个非限定是枚举来关联 员工信息的具体意思,这样就不用自己记忆哪个是id,哪个是name了
enum Info{
    id,
    name
};
std::cout<< std::get<0>(a) <<std::endl;//没使用enum时的取法
std::cout<< std::get<name>(a) <<std::endl;//更方便的取法

如果使用限定作用域的枚举类型来 记录 元组里的东西,会比较复杂

//让枚举类型转为下标类型给get使用
/*
使用underlying_type_t:获取枚举值底层型别
使用constexpr:传入get的模板形参需要在编译期计算出结果
*/
template<class T>
constexpr std::underlying_type_t<T> toUtype(T enumerator) noexcept{
    return static_cast<std::underlying_type_t<T>>(enumerator);
}
std::cout<< std::get<toUtype(Info::name)>(a) <<std::endl;

标识为删除函数,优于声明为private

删除函数最好声明为public,这样可以得到较好的错误信息

所有函数都能成为删除函数,而只有成员函数才能是private。
下面这样写可以避免传入double时隐式类型转换为int

void isLu(int number){
    std::cout<<"int"<<std::endl;
}
void isLu(double number) = delete;//拒绝double和float型别:因为float会优先转为double而不是int

删除函数 可以 阻止不应该进行的模板具现。无法通过给予成员函数模板不同的访问权限来实现,因为模板特化必须写在名字空间作用域,而不是类作用域。

使用成员函数 引用饰词 来区分对左值和右值*this对象的处理

class A{
public:
    void fun() & {
        std::cout<<"*this是左值对象"<<std::endl;
    }
    void fun() && {
        std::cout<<"*this是右值对象"<<std::endl;
    }
};
int main(){
    A a;
    a.fun();//左
    A().fun();//右
    return 0;
}

优先使用const_iterator而不是iterator

auto it = std::find(v.cbegin(),v.cend(), 2);//const_iterator

cbegin()返回的是const_iterator,其实现是:返回begin(const引用类型的容器),而begin是begin(T)的模板形式,因此会调用到其const参数特化版,即返回了const_iterator

...
template<typename _Container>
inline _GLIBCXX17_CONSTEXPR auto
begin(const _Container& __cont) -> decltype(__cont.begin())
{ return __cont.begin(); }

只要不发出异常,就写上noexcept

c++98的vector push_back能够提供强异常安全保证:在空间不够时,会先将元素复制到新内存块,复制完成后才释放旧内存块,执行析构。在c++11中如果使用移动替代复制,有可能会有异常,因此使用的是std::move_if_noexcept,当vector中元素是noexcept的时候才会使用move的方式。

std::move_if_noexcept:如果移动构造函数声明了noexcept,那么就调用移动构造,否则调用拷贝构造。
因此,如果不确定对象是否会抛出异常,或者不确定对象是否支持移动操作,就使用std::move_if_noexcept来保证安全

struct Good
{
    Good() {}
    Good(Good&&) noexcept
    {
        std::cout << "Good&&\n";
    }
    Good(const Good&) {
        std::cout << "const Good&\n";
    };
};
int main()
{
    Good g;
    Good g2 = std::move_if_noexcept(g);//Good&&
}

总结:析构函数本身不应该抛异常,因此一般都将析构函数声明为noexcept
移动构造、移动赋值、交换等函数不抛异常时一定要使用noexcept

constexpr

constexpr表示:如果传入的a是编译期常量,那么返回的值也是编译期常量。否则其值在执行期计算。注意,c++11中,constexpr函数只能传入和返回非void类型的值,在c++14中就可以了。

constexpr size_t fun(size_t a){
    //c++11禁止在其中写多个return,到了14就可以了
    return 200*a;
}
int main()
{
    //1.传入编译期常量
    constexpr size_t m=3;
    std::array<int, fun(m)> res;//fun(m)是编译期常量
    //2.传入变量
    int a;
    std::cin>>a;
    fun(a);//返回的不是constexpr
}

构造函数、访问成员变量的函数、非成员函数都能写成constexpr,这意味着自定义类型对象也能具备constexpr属性。

class A{
public:
    constexpr A(int m): x(m){//由此可以得到constexpr的对象
    }
    constexpr int get_x(){//可以编译期调用,得到constexpr x
        return x;
    }
private:
    int x;
};
constexpr int not_memberfun(int x){//非成员函数也可以在编译期调用
    return x;
}
int main()
{
    constexpr A a(3);
    std::array<int, not_memberfun(a.get_x())> arr;
}

constexpr函数都隐式的被声明成了const的。也就是说,所有constexpr对象都是const对象,而并非所有const对象都是constexpr对象。c++11中,设置成员变量的函数不能是constexpr的,在c++14中就可以了

在多线程环境中使用原子操作atomic来保护单个变量

对于单个要求同步的变量或内存区域,使用 std::atomic就足够了。如果有更多变量需要作为一整个单位进行操作时,才使用互斥锁。

  • 相关操作的原子性

    • std::cout<<a不是原子操作:

      读取原子变量a与调用operator<<是2个步骤。但operator<< int是按值传递的,不用担心其它线程修改了该变量值。

    • 原子变量的自增自减都是原子操作。对比:而使用volatile声明的变量时,系统总是重新从它所在的内存读取数据
  • 使用原子操作可以避免代码被重排

    在代码中对不同独立变量进行赋值的操作,可能执行顺序和代码中不一样

    a = b
    x = y
    //可能重排为:
    x = y
    a = b
    
  • std::atomic的复制操作被删除了,同时没有显式声明移动操作,因此不能直接用于初始化另一个原子变量,而是要使用load()

    std::atomic<int> x{3};
    // auto y = x;//std::atomic的复制操作是被删除掉了的
    std::atomic<int> y(x.load());
    

默认生成的函数

默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符。

  1. 拷贝构造与拷贝赋值相互独立,自己声明了其中一个 不会影响编译器生成另一个。
  2. 移动构造和移动赋值会相互影响,声明了其中一个,那么编译器就不会生成另一个。
  3. 拷贝操作和移动操作会相互影响,声明了其中一个,那么编译器就不会生成另一个。
  4. 声明析构阻止编译器产生默认的移动操作,但不会阻止生成默认的拷贝。

大三律

拷贝构造、拷贝赋值、析构 中声明了任一函数,就必须同时声明其它函数。也就是说,

  1. 声明了析构 就应该自己声明拷贝操作,而不是使用编译器自动生成的。
  2. 移动操作 仅当类中未包含用户显式声明的复制操作、移动操作和析构函数时才生成
  3. 如果只声明了拷贝操作,而没有声明移动操作,那么所有移动请求触发的都是拷贝
  4. 成员函数模板不会抑制其它函数的生成

推荐做法:写了析构函数,就顺手将拷贝和移动使用"=default"来定义

std::auto_ptr:现在用std::unique_ptr代替它

auto_ptr是c++98对智能指针的一次尝试,其使用拷贝操作来完成移动任务,也就是在拷贝构造中执行了复制后,将原对象置空了。这导致其不能存储在容器中,因为容器要求其存储的元素类型能够被拷贝和赋值

创建unique_ptr时需要将一个new 操作符返回的指针传递给它的构造函数。
使用std::unique_ptr作为工厂函数的返回类型非常适合,因为独占指针可以方便的转换成共享指针

#include <iostream>
#include <memory>
//抽象产品
class Logger{
public:
    Logger(){
        std::cout<<"Logger()"<<std::endl;
    }
    virtual ~Logger(){
        std::cout<<"~Logger()"<<std::endl;
    }
    virtual  void writeLog() = 0;
};
//具体产品
class A:public Logger{
public:
    A(){
        std::cout<<"A()"<<std::endl;
    }
    ~A(){
        std::cout<<"~A()"<<std::endl;
    }
    void writeLog(){
        std::cout<<"A writeLog"<<std::endl;
    }
};
//抽象工厂
class LoggerFactory{
public:
    virtual std::unique_ptr<Logger> createLogger() = 0;
    LoggerFactory(){
        std::cout<<"LoggerFactory"<<std::endl;
    };
    virtual ~LoggerFactory(){
        std::cout<<"~LoggerFactory()"<<std::endl;
    }
};
//产品对应的具体工厂实现
class A_LoggerFactory:public LoggerFactory{
public:
    std::unique_ptr<Logger> createLogger(){
        return std::unique_ptr<A>(new A());
    }
    A_LoggerFactory(){
        std::cout<<"A_LoggerFactory"<<std::endl;
    };
    ~A_LoggerFactory(){
        std::cout<<"~A_LoggerFactory()"<<std::endl;
    }
};

int main()
{
    std::unique_ptr<LoggerFactory> factory(new A_LoggerFactory);
    std::unique_ptr<Logger> logger = factory->createLogger();
    logger->writeLog();
    return 0;
}

指定std::unique_ptr的删除器 最好使用 无捕获的lambda表达式,它不会浪费空间。采用函数指针的方式会增大std::unique_ptr的对象尺寸
原因:lambda 表达式其实是个匿名类对象,如果它不捕获任何变量,就是个空的类。这个空的类作为基类的时候,是不需要占据空间的。这种优化被称为空基类优化。

using a_delete = void(*)(A_LoggerFactory*);
std::unique_ptr<LoggerFactory, a_delete> factory(new A_LoggerFactory, [](A_LoggerFactory* p){
    std::cout<<"自定义删除器"<<std::endl;
    delete p;
});

不要使用std::unique_ptr<T[]>,因为使用vector等容器 比使用裸数组 好的多

关于std::shared_ptr

std::shared_ptr的大小是裸指针的2倍:因为它多保存了一个指向控制块的指针。无论用什么方式创建的共享指针,引用计数的内存都必须动态分配。
共享指针

由std::unique_ptr 构建一个 std::shared_ptr时,会创建上述控制块。使用裸指针作为构造函数实参创建std::shared_ptr时,会创建控制块,因此有可能创建多个控制块,产生问题。

移动构造一个std::shared_ptr比复制更快:因为复制要使得引用计数增加。而引用计数的递增和递减是原子操作,比非原子操作慢,所以读写它们的成本较高。

与std::unique_ptr的不同之处:std::shared_ptr的删除器不是其类型的一部分,因此删除器不同的两个共享指针是可以互相赋值的。也因为这个原因,删除器的实现方式不影响std::shred_ptr对象的大小,它始终是裸指针的2倍。
std: :unique_ptr的析构函数 对它指涉的对象调用删除器
std::shared_ptr和 std::weak_ptr的析构函数 对引用计数实施自减

shared_from_this的使用

class A: public std::enable_shared_from_this<A>{
public:
    void fun(){
        // v.emplace_back(this);//这样写就错了!
        v.emplace_back(shared_from_this());
    }
private:
    std::vector<std::shared_ptr<A>> v;
};

一般会将构造函数设为私有,使用一个静态的工厂函数来调用构造函数。

#include <iostream>
#include <memory>
class A{
private:
    //构造函数
    template<class... Ts>
    A(Ts... params){
        std::cout<<"构造"<<std::endl;
        myprint(params ...);
    }
public:
    //使用完美转发、变长模板参数 来 调用 private的 构造函数
    template<class... Ts>
    static std::shared_ptr<A> create(Ts&& ... params);
private:
    //解包
    template<class T0, class... Ts>
    void myprint(T0 t0, Ts ... params){
        std::cout<<t0<<std::endl;
        myprint(params ...);
    }
    template<class T>
    void myprint(T a){
        std::cout<<a<<std::endl;
    }
};
//静态成员函数的实现:直接调用构造函数
template<class... Ts>
std::shared_ptr<A> A::create(Ts&& ... params){
    // return std::make_shared<A>(params ...);//不能使用这种方式,make_shared访问不到private构造函数
    return std::shared_ptr<A>(new A(std::forward<Ts>(params)...));
}
int main()
{
    std::shared_ptr<A> a_p = A::create(3, 4, 5);
    return 0;
}

解决make_shared访问不到private构造函数的问题

class A {
public:
    static std::shared_ptr<A> create() {
        struct make_enable_share :public A{};
        return std::make_shared<make_enable_share>();
    }
private:
    A() {}  
};

总之。最好是使用std:unique_ptr,如果不能胜任,可以由它来创建std::shared_ptr。
使用shared_ptr时,能使用移动操作就不要使用复制操作,这样更快。
最好不要用std::unique_ptr<T[]>。在std::shared_ptr中更不要这样做,它甚至没有提供operator[]

关于std::weak_ptr

使用目的:希望在所指向的对象未失效时访问它,失效时返回nullptr或抛出异常
创建shared_ptr:通过lock(失败返回nullptr)、通过构造函数(失败抛出异常)
引用计数:weak_ptr通过检查控制块里的引用计数来校验自己是否失效(expired函数)。
弱计数:控制块中的弱计数用于对当前的weak_ptr进行计数。
什么时候释放控制块:当引用计数和弱计数都为0时才释放内存。而对象的析构是引用计数为0时发生的,因此对象的析构和内存的释放之间可能会产生延迟。这取决于对象和控制块是不是在一次内存中分配的。
由后续讨论可知,使用std::make_shared是一次性分配内存,这就导致引用计数为0时,对象的内存没能回收,直到弱计数为0才并释放控制块并回收整块内存。而std::shared_ptr(new xx())是分2次分配内存的,对象所占内存就能在引用计数为0时回收。

用于工厂函数:在前面的介绍中使用std::unique_ptr来作为工厂函数的返回值,如果工厂函数中 创建并初始化对象的成本高昂,可以使用std::shared_ptr,搭配std::weak_ptr来实现一个带缓存的工厂函数。

std::unordered_map<int, std::weak_ptr<Logger>> cache;
...
//产品对应的具体工厂实现
class A_LoggerFactory:public LoggerFactory{
public:
    std::shared_ptr<Logger> createLogger(int id){
        auto objPtr = cache[id].lock();
        if (!objPtr)
        {
            std::cout<<"创建"<<std::endl;
            objPtr = std::shared_ptr<A>(new A());
            cache[id] = objPtr;
        }else{
            std::cout<<"cache"<<std::endl;
        }
        return objPtr;
    }
    A_LoggerFactory(){
        std::cout<<"A_LoggerFactory"<<std::endl;
    };
    ~A_LoggerFactory(){
        std::cout<<"~A_LoggerFactory()"<<std::endl;
    }
};

用于观察者模式:可以在通知观察者之前检查指针是否空悬

#include <iostream>
#include <memory>
#include <list>
//观察者
class Observer{
public:
    virtual void aftersee() = 0;
};
//具体观察者
class ObserverA:public Observer{
public:
    void aftersee(){
        std::cout<<"see_A"<<std::endl;
    }
};

//主题:会改变状态
class Subject{
public:
    void fun(){
        //...
        //通知观察者:通知前检查指针是否空悬
        for (auto l:ls)
        {
            if (!l.expired()){
                l.lock()->aftersee();
            }
        } 
    }
    void addObserver(std::weak_ptr<Observer> wpt){
        ls.push_back(wpt);
    }
private:
    std::list<std::weak_ptr<Observer>> ls;
};

int main()
{
    std::shared_ptr<Subject> subject = std::make_shared<Subject>();
    std::shared_ptr<Observer> observerA = std::make_shared<ObserverA>();
    subject->addObserver(std::weak_ptr<Observer>(observerA));

    observerA = nullptr;//这里将observerA释放了,fun中会检测到指针空悬

    subject->fun();
    return 0;
}

父节点中保存指向子节点的指针:使用std::unique_ptr,因为子节点通常只被父节点拥有
子节点中保存指向父节点的指针:直接使用裸指针,因为子节点生命周期不会比父节点长

优先使用std: :make_shared,而不是直接new

std::make_unique(c++14) 和 std: :make_shared(c++11)都是将实参完美转发给 动态分配内存的对象 的 构造函数。

std::make_shared的优点

  • 避免内存泄漏
    下面2种写法,第一种由于new Widget、赋值给share_ptr、computePriority的执行顺序不定,在computePriority发生异常时可能会内存泄漏。而第二种不会内存泄漏。
    processWidget(std::shared_ptr< Widget>(new Widget), computePriority());
    processWidget (std::make_shared< Widget>(), computePriority());
  • 提升性能
    make_shared只分配一次内存,用于保存对象、控制块。而使用new的方式需要2次内存分配

不使用make系列函数的情形

  • 想要自定义删除器
  • 参数是小括号形式,而不是花括号初始化的方式。
    auto ptr = std::shared_ptr<std::vector<int>>(new std::vector<int>{2,3});
    auto initList = {2,3};
    auto ptr2 = std::make_shared<std::vector<int>>(initList);
    
  • 对象很大,可能会出现回收延迟的问题

如果确实要使用std::shared_ptr(new xx)的方式,需要注意可能的内存泄漏、尽量使用移动操作来保证高效。

#include <iostream>
#include <memory>
void fun(std::shared_ptr<int> spw, int n){
    //...
}
int fun_cause_error(){
    throw "xxx_error";
    return 2;
}
int main()
{
    //1.危险的方式:可能造成内存泄漏,
    //也就是new了以后还没来得及交由共享指针来管理,fun_cause_error就抛出异常了
    fun(std::shared_ptr<int> (new int(3)), fun_cause_error());
    //2.安全但不够高效的方式:这里sp是左值,传参时发生一次shared_ptr拷贝
    std::shared_ptr<int> sp(new int(3));
    fun(sp, fun_cause_error());
    //3.安全高效:使用std::move将左值转为右值,避免了shared_ptr拷贝
    std::shared_ptr<int> sp2(new int(3));
    fun(std::move(sp2), fun_cause_error());
    return 0;
}

Pimpl (指向实现的指针) 习惯用法

用一个指向结构体的指针 来替代 类的数据成员, 以提升编译速度。
在下述的示例中,需要注意,使用了std::unique_ptr也仍然要写析构函数,避免使用编译器默认生成的析构。

  1. 在头文件中声明但不定义Impl数据类型,使用智能指针来管理
    //widget.h
    #include <memory>
    class Widget{
    public:
        Widget();//为pImpl分配内存
        ~Widget();//使用智能指针,不需要再手动释放pImpl。
        //但仍然必须保证在cpp中定义析构,而不是使用编译器自动生成的。
        //否则析构时检查到Widget::Impl是不完整类型而报错
    private:
        class Impl;//声明,在cpp里实现
        std::unique_ptr<Impl> pImpl;//使用智能指针,而不是Impl *pImpl
    };
    
  2. 在cpp文件中真正定义该数据
    #include "widget.h"
    #include <string>
    #include "gadget.h"//对gadget.h的依赖 从 widget.h 转移到了 widget.cpp中
    class Widget::Impl{
        std::string name;
        int id;
        GadGet gg;
    };
    Widget::Widget(): pImpl(std::unique_ptr<Impl>(new Impl)){
    }
    Widget::~Widget(){
    }
    

由于上述代码声明了析构函数,因此会阻止生成默认的移动操作
仍然需要在cpp中定义,避免 不完整类型 的问题

//widget.h
Widget(Widget&& rhs);//如果抛出异常,会生成析构代码,而析构需要保证pImpl类型完整
Widget& operator=(Widget&& rhs);//重新赋值前会析构原对象,因此也会检查类型

//widget.cpp
Widget::Widget(Widget&& rhs) = default;
Widget& Widget::operator=(Widget&& rhs) = default;

由于Widget包含的std::unique_ptr 是只移型别,因此不会生成默认的拷贝操作,如下可以进行手动实现。

Widget::Widget(const Widget& rhs){
    std::unique_ptr<Impl> uptr(rhs.pImpl.get());

}
//Widget里的std::unique_ptr是只移型别,因此没法生成默认的拷贝,这里手动实现
Widget& Widget::operator=(const Widget& rhs){
    *pImpl = *rhs.pImpl;
    return *this;
}

总而言之,在“指向实现的指针”语法中,为了效率会优先使用std::unique_ptr,并注意实现析构函数和移动操作,如果要使用拷贝操作,也需要手写实现。
如果使用shared_ptr,能避免上述麻烦。

std::move和std::forward

std::move无条件将实参强转成右值,而std::forward仅在特定条件下强转

std::move:首先使用remove_reference去除引用,再将&&应用在这个非引用型别上,这样得到的结果必定是右值引用。它的作用是使对象具备可移动的条件,而不是做移动操作

template<class T>
decltype(auto) my_move(T&& param){
    using T_type = typename std::remove_reference<T>::type;//去除引用类型
    return static_cast<T_type&&>(param);//强转成右值引用类型
}

想要对象能移动,就不要将其声明为const,否则仍然调用的是拷贝操作。因为常量左值引用 可以引用 常量右值

class A{
public:
    A(const int& m){
        std::cout<<"A(const int& m)"<<std::endl;
    }
    A(int&& m){
        std::cout<<"A(int&& m)"<<std::endl;
    }
};
const int&& m = 3;
A a(m);//调用的是A(const int& m) 而不是移动构造

传递给std::forward的实参型别 应当是个非引用型别

区分右值引用和万能引用:万能引用必须形如T&&且T的类型是推导而来

  1. const T&&是右值引用
  2. vector中的push_back使用的是右值引用,emplace_back使用的万能引用。
    因为push_back(T&& x的T类型就是vector的T类型,当vector确定T以后,push_back也就确定了,这里push_back并没有发生对T的推导。而emplace_back(Args&&… args)的Args是独立于vector来专门声明的类型
    对比
  3. 声明为auto&&的变量 都是万能引用

区分好 右值引用 与 万能引用 后。
转发 右值引用 时,使用std::move来进行无条件的向右值强制类型转换。
转发 万能引用 时,使用std::forward。

万能引用替代 左值引用和右值引用的重载函数
对于想要同时提供左值引用、右值引用版本的函数, 直接将其参数写成万能引用。
前者要创建string临时对象,然后将临时对象移动给m。后者直接将字面量传递给m,效率更高。

//1.
void fun(const std::string& s){
		m = s;
    std::cout<<"const std::string& s"<<std::endl;
}
void fun(std::string&& s){
		m = std::move(s);
    std::cout<<"std::string&& s"<<std::endl;
}
//2.
template<class T>
void fun(T&& s){
    m = std::forward<T>(s);
    std::cout<<"T&&"<<std::endl;
}

在函数中,为了保证某对象在完成 某些操作之前 不被移走,就需要注意在最后一次使用该对象时 写std::move或std::forward

在按值返回的函数中,是否要实施std::move 或者 std::forward

  1. 返回的是绑定到一个右值引用或一个万能引用的对象,则应该使用std::move/std::forward以提高效率

  2. 返回局部变量本身,且它的类型和函数返回值一致时,编译器会进行优化。因此这种情况不要使用std::move,否则返回的是变量的引用,而非它本身,就不会进行rvo优化。

rvo和nrvo

  • RVO(返回值优化)
    将“返回一个类对象的函数”的返回值 当做该函数的参数处理,这样就避免了 函数返回值时创建临时对象 然后析构 的 开销。
    如果不适合进行复制省略,就会隐式的使用std::move。因此,无论如何也没有必要自己给该变量写std::move
  • NRVO(命名的返回值优化)
    RVO是对程序员手动返回的对象进行优化,但仍然需要程序员创建和返回值同类型的变量,NRVO中直接把这个变量也优化掉了。

不要重载 形参为万能引用的函数

template<class T>
void fun(T&& t){
    std::cout<<"T&&"<<std::endl;
}

void fun(float n){
    std::cout<<"float"<<std::endl;
}

int main()
{
    fun(3.5);//T&&   double被T&&精确匹配
    fun(3.5f);//float

不要写完美转发构造函数。即使写了完美转发构造函数,编译器仍然会生成拷贝和移动构造函数

A a1 = a正常来说会调用拷贝构造函数A(const A& rhs),但是a如果是非const对象,需要转为const, 而这里有完美转发构造函数,就会优先调用完美转发构造函数。
若在函数调用时,一个模板实例化函数和常规函数具备相等的匹配程度,则优先选用常规函数

class A{
public:
    template<class T>
    A(T&& t){
        std::cout<<"A(T&& t)"<<std::endl;
    }
		//如果写成A(A& rhs),A a1 = a且a为非const对象时才会优先调用它
    A(const A& rhs){
        std::cout<<"A(const A& rhs)"<<std::endl;
    }
};

int main()
{
    A a(3);//A(T&& t)
    A a1 = a;//A(T&& t)

当需要对绝大多数的实参型别实施转发,某些型别特殊处理时,使用标签分派的方法

既想有万能引用的性能,又想使用重载时,并不对原始函数进行重载,而是多写一个Impl函数,它多带了一个std::true_type/std::false_type的标签参数

#include <iostream>

template<class T>
void funImpl(T&& t, std::false_type){
    std::cout<<"普遍情况"<<std::endl;
}

void funImpl(int m, std::true_type){
    std::cout<<"参数为int时"<<std::endl;
}

template<class T>
void fun(T&& t){
    using type_raw =  typename std::remove_reference<T>::type;//去除引用的类型
    funImpl(std::forward<T>(t), std::is_integral<type_raw>());
}

int main()
{
    fun(3);//参数为int时
    fun(3.1);//普遍情况
    return 0;
}

使用 std::enable_if 解决 完美转发构造函数 的问题

标签分配 不能解决 完美转发构造函数,可能被编译器自动生成的移动构造绕过。

class A{
public:
    template<class T>
    A(T&& t){
        A_impl(std::forward<T>(t), std::is_integral<typename std::remove_reference<T>::type>());
    }

    template<class T>
    void A_impl(T&& t, std::false_type){
        std::cout<<"_impl(T&& t, std::false_type)"<<std::endl;
    }

    void A_impl(int t, std::true_type){
        std::cout<<"int "<<std::endl;
    }
};
int main()
{
    A a(3);//int
    A a1(std::move(a));//调用编译器自动生成的 移动构造, 导致自己在构造函数写的标签分配 失效

使用 std::enable_if 来禁用一部分函数模板

template<class T>
typename std::enable_if<!std::is_same<T, int>::value, T>::type fun(T t){
    return t+t;
}

int fun(int a){
    return a;
}

int main()
{
    std::cout<< fun(2) <<std::endl;//2
    std::cout<< fun(std::string("2")) <<std::endl;//22
    return 0;
}

使用std::decay去除const、volatile,使用std::is_base_of确保子类对象调用父类构造函数时调用的是完美转发构造函数

在这里插入代码片

std::rank和std::extent的使用

std::rank获取数组的维度数,std::extent可以知道指定维度下有多少个元素

int a[3][1] = {{1},{4},{4}};
std::cout<<"数组维度"<< std::rank<decltype(a)>::value <<std::endl;//2
std::cout<<"数组长度"<< std::extent<decltype(a), 1>::value <<std::endl;//1
//去除第一维
using a_type = std::remove_extent<decltype(a)>::type;
std::cout<<"数组维度"<< std::rank<a_type>::value <<std::endl;//1
std::cout<<"数组长度"<< std::extent<a_type>::value <<std::endl;//1

std::remove_all_extents可以去除所有维度,得到元素类型。其实现利用了模板偏特化。

//主模板
template<class T>
struct my_remove_all{
    using type = T;
};

//特化:多维数组,递归移除
template<class T, size_t Ix>
struct my_remove_all<T[Ix]>{
    using type = typename my_remove_all<T>::type;
};

//特化:一维数组
template<class T>
struct my_remove_all<T[]>{
    using type = typename my_remove_all<T>::type;
};

std::decay:转换为衰变类型

template<class T>
struct decay
{
private:
    //去除引用类型
    using U = typename std::remove_reference<T>::type;
public:
    using type =  typename std::conditional< 
        std::is_array<U>::value,
        typename std::remove_extent<U>::type*,//如果是数组,就返回元素类型
        typename std::conditional< //不是数组就看是不是函数
            std::is_function<U>::value,
            typename std::add_pointer<U>::type,//是函数就返回函数指针
            typename std::remove_cv<U>::type//除此以外的类型直接擦除const、volatile
        >::type
    >::type;
};

decay的使用例子

std::vector<int> v{1,2};
std::cout<< std::is_same<decltype(v)::value_type, int>::value <<std::endl;
std::cout<< std::is_same<std::decay<decltype(*v.begin())>::type, int>::value <<std::endl;

引用折叠

  • 模板实例化发生引用折叠

    std::forward接收左值引用时,返回左值引用。接收右值引用返回右值引用。
    由函数返回的右值引用 是 定义为右值的

    当传入左值时,参数类型推导为左值引用Widget&,在返回值处发生引用折叠
    当传入右值时,参数类型推导为右值Widget,在返回值处返回Widget&&

    template<class T>
    T&& myforward(typename std::remove_reference<T>::type& param){
        return static_cast<T&&>(param);
        //传左值等价成了static_cast<Widget&& &>(param)
        //传右值等价成了static_cast<Widget&&>(param)
    }
    
  • auto型别推导发生引用折叠

    auto也同样有引用折叠
    m是左值,推导类型auto是左值引用,与&&折叠后为 左值引用
    fun()是右值,推导类型auto是int,折叠后为右值引用

    int m = 3;
    auto&& x = m;
    auto&& y = fun();
    
  • 使用别名时出现引用的引用 也会导致引用折叠

    using a_type = int&&;
    using b_type = a_type&;//int&
    
  • decltype中也会出现引用折叠

通常情况下,先假定不存在移动操作、移动成本高、不能使用到移动操作

也就是说,操作尚不清楚的某对象时,一开始应该先假定它没有提供移动操作,即使有移动操作也可能很慢,或者会因为异常安全问题而不被调用到。

  • std::array的移动并不比复制快

    std::array没有一个指向该容器的堆内存指针,因为其内容数据是直接存储在对象内的,不需要额外的堆内存空间,其实际上分配在栈上。因此其移动操作只是对元素的逐一移动,速度慢。

  • std::string的移动并不比复制快

    它使用了小型字符串优化SSO,因此并不是所有std::string都分配在堆上,小字符串会分配在 基于栈的一小块缓冲区上。因此对小字符串的移动操作并不比复制快。

  • vector容器的强异常安全保护可能使得编译器调用复制操作而非更高效的移动操作

熟悉完美转发的失败情形

  • 传 {}

    T&&无法推导大括号初始化物,实在是想用{},就使用auto作为局部变量,再传入到T&&函数

    void f(const std::vector<int>& v){
        std::cout<<"const std::vector<int>& v"<<std::endl;
    }
    
    template<class T>
    void fun(T&& t){
        f(std::forward<T>(t));
    }
    
    
    int main()
    {
        std::vector<int> v ={1,2,3};
        fun(v);
        // fun({1,2,3});//报错
    }
    
  • 传 静态常量成员变量

    静态常量成员变量通常只需声明,无需在类外定义。static const 成员变量 是在整个类中都恒定的常量。const 成员变量 只在对象生命周期内是常量,相对于类来说是可变的,因为一个类可以创建多个对象。
    但如果作为参数传给T&&无法完成链接,因为引用实际上就是指针,而指针就要求该变量有某块内存去指向。
    解决方法:提供静态常量成员变量的定义

    class A{
    public:
        static const int num=3;//声明
    };
    //const int A::num;//应当添加该定义,但这里无需重复指定初始化物
    int main()
    {
        std::cout<< A::num <<std::endl;//直接使用,编译器在编译时将其替换为常量值
        fun(A::num);//报错:undefined reference to `A::num'
    }
    

lambda表达式中不要使用默认捕获,应当显式的列出所捕获变量

  • 如果确定闭包会被立即使用,那就放心使用&捕获

    按引用捕获局部变量可能导致指针空悬

    //定义 存储函数指针的容器
    using fc = std::vector<std::function<bool(int)>>;
    fc fc_1;
    //添加到容器的lambda函数捕获了局部变量m
    void test1(){
        int m = fun(3);
        fc_1.push_back([&](int v){
            return v % m == 0;
        });
        // std::cout<<fc_1[0](6)<<std::endl;//1
    }
    int main()
    {
        test1();
        //在test1外调用容器中保存的函数,但是局部变量m已经被释放了
        std::cout<<fc_1[0](6)<<std::endl;//error:Floating point exception
    }	
    

    std::all_of的使用:algorithm.h

    std::vector<int> v= {2,2,2};
    std::cout<<std::all_of(v.begin(), v.end(), [](int x){
         return x == 2;
    })<<std::endl;//1
    
  • 捕获成员变量:按值捕获this指针容易导致指针空悬

    在下面的示例中,智能指针调用add()添加完函数后就释放了,如果add()函数中lambda捕获的是this,就会发现指针空悬了,无法得到正确的this->m值。因此需要提前保存要使用的成员变量值。

    #include <iostream>
    #include <vector>
    #include <memory>
    #include <functional>
    #include <algorithm>
    //存放函数的容器
    std::vector<std::function<void(int)>> v;
    struct A
    {
       int m = 3;
       void add(){//A类提供添加函数的方法:
          auto m_temp = this->m;//把要使用的成员变量复制到局部变量,避免智能指针释放导致指针空悬
          v.push_back([m_temp](int n){
             std::cout<<m_temp<<" "<<n<<std::endl;
       });
       }
    };
    void test(){
       auto a_ptr  = std::unique_ptr<A>(new A);
       a_ptr->add();//添加一个void(int)函数
    }//test执行完毕,a_ptr被释放,this指针也被释放
    int main()
    {
       test();
       v[0](4);//3 4
    }
    
  • 捕获成员变量的好方法:最好使用c++14中的广义捕获(初始化捕获),可以结合std::move实现移动捕获

    struct A
    {
       int m = 3;
       void add(){//A类提供添加函数的方法:
          v.push_back([m = m](int n){//广义捕获
             std::cout<<m<<" "<<n<<std::endl;
       		});
       }
    };
    
  • 不能按值捕获static变量:只需要通通视作按引用捕获

    平时最好不要像下面这样写默认的按值捕获,否则会误以为捕获了static变量,误以为lambda函数内的操作不会影响到真实的m值。

    std::vector<std::function<void()>> v;
    void fun(){
       static int m=1;
       v.push_back([=](){//实际上并没有按值捕获到m,因此每次调用fun都会真实的影响v中所有的m值
          m++;
          std::cout<<m<<std::endl;
       });
    }
    int main()
    {
       fun();
       fun();
       v[0]();//2
    }
    

想要将 只移对象 放入闭包:移动捕获

  • c++14直接支持移动捕获

    #include <iostream>
    #include <functional>
    struct A
    {
       A(){
          std::cout<<"A()"<<std::endl;
       }
       A(const A& a){
          this->m = a.m;
          std::cout<<"A(const A& a)"<<std::endl;
       }
       A(A&& a){
          this->m = a.m;
          a.m = 0;
          std::cout<<"A(A&& a)"<<std::endl;
       }
       ~A() = default;
       int m;
    };
    std::function<void(int)> test(){
       A a;//A()
       a.m = 4;
       return [a = std::move(a)](int n){//move:A(A&& a) return:A(A&& a)
          std::cout<<a.m<<" "<<n<<std::endl;
       };
    }
    int main()
    {
       test()(3);//4 3
    }
    
  • c++11中使用bind实现移动捕获

    auto func = lambda创建了一个绑定好参数的对象后,闭包和参数的生命周期就是一样的。

    #include <iostream>
    #include <functional>
    #include <vector>
    void test(){
    	std::vector<int> v={1,2,3};
    	//c++14移动捕获
    	auto func = [data = std::move(v)](){
    		for(auto item:data){
    			std::cout<<item<<std::endl;
    		}
    	};
    	//c++11中使用bind实现移动捕获:std::move(v)是个右值实参,
    	//因此绑定到lambda函数对象时是使用的移动构造
    	auto func2 = std::bind([](const std::vector<int>& data){
    		for(auto item:data){
    			std::cout<<item<<std::endl;
    		}
    	}, std::move(v));
    
    	func2();
    }
    

lambda表达式中使用完美转发时,型别使用decltype推导即可

这里的auto&&是万能引用,因此传左值时,首先推导出类型为左值引用,decltype取得的是同样类型,因此decltype(param)是左值引用,forward的实现中会首先使用std::remove_reference得到类型int,然后推导得参数类型为int&,int&与&&进行引用折叠,得到int&。

#include <iostream>
void test(int& a){
	std::cout<<"int&"<<std::endl;
}
void test(int&& a){
	std::cout<<"int&&"<<std::endl;
}
int main()
{
	auto f = [](auto&& param){
		return test(std::forward<decltype(param)>(param));
	};
	int a=3;
	f(a);//int&
	f(3);//int&&
}

优先使用lambda而非std::bind

  • std::literals字面量(c++14)的使用

    #include <iostream>
    #include <thread>
    // #include <chrono>
    using namespace std::literals;
    int main()
    {
    	for (size_t i = 0; i < 100; i++)
    	{
    		std::cout<<i<<std::endl;
    		// std::this_thread::sleep_for(std::chrono::seconds(1));
    		std::this_thread::sleep_for(1s);//和上述等价
    	}
    }
    
  • lambda代替std::bind

    std::bind的问题:下面的3作为参数传给了std::bind函数而非fun函数,如果传的不是3而是now(),那么这个时间就是调用bind的时间,而非调用fun的时间

    auto fun_bind = std::bind(fun, 3, std::placeholders::_1);
    fun_bind(4);
    
    auto fun_lambda = [](int b){
    	fun(3, b);
    };
    fun_lambda(4);
    
  • std::bind传参默认是按值传递,除非使用std::ref()

void test(int& x, int y){
    x = 3;
}
int main()
{
    int m = 0;
    auto fun = std::bind(test, std::placeholders::_1, 22);
    fun(std::ref(m));
    std::cout << m << std::endl; // 输出 3
}	

总之:std::bind只有2种用处,都是用来弥补lambda表达式的不足

  1. 在c++11中,使用它和lambda表达式来模拟 移动捕获
  2. 在c++11中,使用它来实现 lambda带auto形参的情形

并发API

首先要意识到,异步运行一个函数有2种方式。

  1. 基于线程:使用std::thread创建一个线程,并在其上运行该函数
  2. 基于任务:把函数视作任务,把任务传给std::async()

基于任务的方法 比 基于线程的方法更好:优先使用std::async而不是std::thread

  1. std::async()有返回值,可以通过get获取。而std::thread没有
  2. 使用std::thread需要担心软件线程用尽抛出std::system_error异常,而std::async()不一定会创建一个新线程来执行,它可能会直接在调用get()的地方执行。
#include <iostream>
#include <thread>
#include <future>
//需要异步执行的函数
int fun(){
    int sum=0;
    for (int i = 0; i < 3; i++)
    {
        sum += i;
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    return sum;
}

int main()
{
    auto f = std::async(fun);
    for (int i = 0; i < 5; i++)
    {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::cout<<"主线程做点其它事"<<std::endl;
    }
    std::cout<<f.get()<<std::endl;//需要获取异步函数的执行结果了,如果仍在执行就阻塞等待执行结果
}
    

总之,尽量使用std::async,除非是特定领域下 想优化线程使用方法

std::async有以下3种启动策略

1.std::launch::async:异步执行,函数必须在另一线程执行
2.std::launch::deferred:延迟执行,在调用get/wait时执行
3.默认启动策略:异步或延迟

在下述代码中指定策略为异步或延迟,也就和默认的其实是一样的

#include <iostream>
#include <thread>
#include <future>
//需要 异步/推迟 执行的函数
int fun(int m){
    return m*2;
}
int main()
{
    auto f = std::async(std::launch::async | std::launch::deferred, fun, 3);
    std::cout<<f.get()<<std::endl;
} 

  1. 默认策略会导致无法预知函数是否已执行,是否会执行。可能使得thread_local变量的值不确定,因为你不知道读取到的是哪个线程的局部存储
  2. 默认策略可能导致函数推迟执行,如果没有线程调用get,就永远不会执行。
    在下面代码中,f永远不会被执行到
int main()
{
    auto f = std::async(std::launch::async | std::launch::deferred, fun, 3);
    while (f.wait_for(1s) != std::future_status::ready)
    {//始终循环
        /* code */
    }
    std::cout<<f.get()<<std::endl;//永远不会执行到
}
  • wait_for()一段时间内等待任务完成

    返回值为 std::future_status 枚举类型,表示任务的状态:
    ready: 任务已完成, timeout: 等待时间已到,任务仍未完成, deferred: 任务尚未开始执行。

获取函数/可调用对象 的 返回值类型

值得注意的是,下面代码不会输出xx

#include <iostream>
#include <functional>
int fun(){
    std::cout<<"xx"<<std::endl;
    return 3;
}
int main()
{
    using f = std::function<int(void)>;
    std::cout<<std::is_same< std::result_of<f()>::type, int >::value<<std::endl;//1
    std::cout<<std::is_same< decltype(fun()), int >::value<<std::endl;//1
}

如下 写一个默认使用std::launch::async启动策略的函数

template<class F, class... Ts>
std::future<typename std::result_of<F(Ts...)>::type> real_async(F&& f, Ts&&... param){
    return std::async(std::launch::async, std::forward<F>(f), std::forward<Ts>(param)...);
}

总而言之,需要异步执行的函数,最好是写成std::async(std::launch::async, f, args)

线程的可联结与不可联结

1.可联结线程:所对应的底层线程阻塞等待运行、或已经运行完毕
2.不可联结线程:已经join联结、已经detach分离、底层线程移动至另一个thread对象、默认构造的std::thread(无可执行的函数)。总之,可联结->不可联结的方式有join、detach、move

  • 委员会规定:可联结线程的析构函数一旦执行,程序就立即终止,原因如下
    1. 如果在析构函数里join等待,那么对于只想要特定情况join线程时,就会出现情况已经不满足了,但仍然等待着,而不是合理的结束调用方线程,导致性能异常
    2. 如果在析构函数里detach分离线程,假设被分离线程捕获调用方线程的局部变量的引用Val,当调用方结束后,执行新函数可能使用到这一片内存空间,使得被分离线程莫名其妙修改到新函数的局部变量值,导致未定义行为
  • 标准库中不存在std::thread的RAII类,但可以自己写一个
    #include <iostream>
    #include <functional>
    #include <thread>
    class ThreadRAII{
    public:
        enum class action{
            join,
            detach
        };
        ThreadRAII(std::thread&& t, action a): m_t(std::move(t)), a(a){
        }//2个参数,第1个是构造的对象、第2个是其销毁动作
        ~ThreadRAII(){
            if (m_t.joinable())//对于不可联结的对象,不要调用join、detach,否则产生未定义行为
            {
                if (a == action::join)
                {
                    m_t.join();
                }else{
                    m_t.detach();
                }  
            }
        }
        ThreadRAII(ThreadRAII&&) = default;//自行实现了析构函数,这使得编译器不会自动生成移动操作
        std::thread& get(){//仿造智能指针的做法,提供get()返回底层的std::thread对象
            return m_t;
        }
    private:
        std::thread m_t;
        action a;
    };
    int main()
    {
        ThreadRAII ta(std::thread([](){
            std::cout<<"xx"<<std::endl;
        }), ThreadRAII::action::join);
        return 0;
    }
    

future, promise以及async的用法

  • get和wait都是一个阻塞调用,但前者会返回任务结果,且会消耗掉std::future对象的状态
  • std::package_task用于包装任务,std::future用于存储任务返回值
    #include <iostream>
    #include <functional>
    #include <future>
    int main()
    {
        //包装要执行的任务
        std::packaged_task<int(int)> task([](int m){
            return 3*m;
        });
        //获取存储返回值的future对象
        std::future<int> res = task.get_future();
        //在新线程上执行任务
        std::thread t(std::move(task), 4);
        t.detach();
        //阻塞等待任务完成
        auto val = res.get();
        std::cout<<"val:"<<val<<std::endl;
        return 0;
    }
    
  • std::promise用来设置值将其传递给另一个线程,例如std::async启动异步任务时就是在内部创建了一个该对象,并使用set_value设置值

std::async的被调函数执行结果 会存放在 共享状态中

  • 被调函数结果的传递方式

    被调函数将结果写入std:;promise,在传递给调用方时,是将结果存储在共享状态处,共享状态通常在堆上。调用方通过std::future读出结果。

  • 委员会规定:std::async启动的非推迟任务会被实施一次隐式join

    这条规则和此前规则 共同保证了async启动策略的任务 不被简单粗暴的结束,又避免了deferred策略的任务迟迟不结束 导致未定义行为。因此,下面代码中main()仍然会等待任务执行完毕。

    #include <future>
    #include <iostream>
    using namespace std::literals;
    int main() {
        int m=3;
        auto fu = std::async(std::launch::async, [](int& m){
            m = 4;
            while (1)
            {
                std::cout<<m++<<std::endl;
                std::this_thread::sleep_for(1s);
            }
        }, std::ref(m));
        return 0;
    }
    

    对于使用std::async启动策略的任务,最后一个std::future对象析构时会阻塞直到任务完成(相当于join),此前的对象都是只析构对象本身而不会等待线程执行完毕(相当于detach)。

  • 创建的std::future对象是否有效:使用valid()判断

    std::future<int> f, f2;//f和f2都是无效的
    f = std::async(std::launch::async, [](){return 3;});//f变得有效了
    
    // std::cout<<f.get()<<std::endl;//读取后释放共享内存
    
    std::shared_future<int> f_shared = f.share();//显式转换为shared_future 此后f失效
    std::cout<<f_shared.get()<<std::endl;//可以多次读取
    std::cout<<f_shared.get()<<std::endl;
    

一个任务告知另一个任务发生了特定事件

在下面代码中,使用条件变量实现。需要注意,wait中使用lambda捕获变量作为阻塞条件时,需要注意按引用捕获

#include <future>
#include <iostream>
#include <condition_variable>
#include <mutex>
using namespace std::literals;
class A{
public:
    void test(){
        auto f1 = std::async(std::launch::async, [&cv = cv, &m_mutex = m_mutex, &i = i](){
            
            while (1)
            {
                i = i-1;
                std::this_thread::sleep_for(1s);
                std::cout<<i<<std::endl;
                std::unique_lock<std::mutex> locker(m_mutex);
                cv.wait(locker, [&i](){//注意这里需要捕获引用
                    return i>0;
                });
                std::cout<<std::this_thread::get_id()<<"工作"<<std::endl;
            }
        });

        auto f2 = std::async(std::launch::async, [&cv = cv, &m_mutex = m_mutex, &i = i](){
            while (1)
            {
                std::this_thread::sleep_for(5s);
                std::cout<<"当前i"<<i<<std::endl;
                i = 10;
                std::unique_lock<std::mutex> locker(m_mutex);//为互斥量加锁
                std::cout<<"唤醒其它线程 i="<<i<<std::endl;
                cv.notify_one();
            }   
        });
    }
private:
    std::mutex m_mutex;
    std::condition_variable cv;
    int i = 3;
};
int main() {
    A a;
    a.test();
    
    return 0;
}

如果f2在执行notify时,f1恰好还没运行到wait处,就会错过唤醒。如果f2的设计中不会再次唤醒其它线程,那么f1将始终阻塞。因此,如果是单次的2个线程同步,仅仅使用条件变量无法保证线程的合理执行顺序,有如下2种解决方法

  1. 多设置一个bool标志,并使用锁来阻止该标志的并发访问

    #include <future>
    #include <iostream>
    #include <condition_variable>
    #include <mutex>
    using namespace std::literals;
    class A{
    public:
        void test(){
           auto f1 = std::async(std::launch::async, [&flag = flag, &cv = cv, &m_mutex = m_mutex](){
            std::lock_guard<std::mutex> lock(m_mutex);//对标志位flag的访问 加锁
            std::cout<<"步骤1:检查"<<std::endl;
            flag = true;
            cv.notify_all();
           });
    
           auto f2 = std::async(std::launch::async, [&flag = flag, &cv = cv, &m_mutex = m_mutex](){
            
            std::unique_lock<std::mutex> u_lock(m_mutex);
            cv.wait(u_lock, [&flag = flag](){
                return flag;
            });//lambda应对虚假唤醒
            std::cout<<"步骤2"<<std::endl;
           });
        }
    private:
        std::mutex m_mutex;
        std::condition_variable cv;
        bool flag = false;//标志位:判断check是否执行过了
    };
    int main() {
        A a;
        a.test();
        return 0;
    }
    
  2. 让步骤2任务 等待 步骤1任务的 期值:即调用wait阻塞等待对方执行完毕。

    由于async实际上是将结果存放在共享状态中,因此产生了在堆上进行分配和回收的成本。

    void test(){
       auto f1 = std::async(std::launch::async, [](){
        std::cout<<"步骤1:检查"<<std::endl;
       });
       auto f2 = std::async(std::launch::async, [&f1 = f1](){
        f1.wait();
        std::cout<<"步骤2"<<std::endl;
       });
    }
    
  3. 方法2 的另一种实现方法:不使用async启动,而是使用std::promise。这种情况不会隐式join,需要手动join

    void test(){
        std::future<void> f = p.get_future();
        std::thread th([&f](){
            f.wait();
            std::cout<<"thread run"<<std::endl;
        });
        p.set_value();
        th.join();
    }
    
  4. 阻塞多个任务

    class A{
    public:
        void test(){
            std::shared_future<void> sf = p.get_future().share();
            for (int i = 0; i < 5; i++)
            {
                vt.emplace_back([sf](){
                sf.wait();//阻塞等待调用sf.get或p.set_value
                std::cout<<"后续步骤..."<<std::endl;
                });
            }
            p.set_value();
    
            for(auto& t:vt){
                t.join();
            }
        }
    private:
        std::promise<void> p;
        std::vector<std::thread> vt;
    };
    

对函数实参提供拷贝和移动操作 的 正确方式

如果希望针对左值实参实施复制,右值实参实施移动,有如下方式。

  • 写2个重载函数

    class A{
    public:
        void fun(const int& m){
            std::cout<<"接受左值,采用复制的方式"<<std::endl;
            v.push_back(m);
        }
        void fun(int&& m){
            std::cout<<"接受右值,采用移动的方式"<<std::endl;
            v.push_back(std::move(m));
        }
    private:
        std::vector<int> v;
    };
    
  • 写成接受万能引用的函数模板

    template<class T>
    void fun(T&& m){
        v.push_back(std::forward<T>(m));
    }
    

    C++中模板必须在头文件中实现,因为编译器需要可见的实现以生成模板具体实例的代码。模板分离技术是指将模板的实现放在另一个头文件里。

    缺点:可能传入不正确的参数类型,模板生成多个不正确类型的函数

  • 按值传递

    对于形参为std::string的函数,c++11引入了字符串的移动构造,当传入的参数是左值时使用复制构造函数构建,传入右值时使用移动构造函数。

    下面代码中fun(My_String ms)采用普通的按值传递,所传元素m为左值时会使用拷贝构造,如果是右值时,使用移动构造。

    #include <iostream>
    #include <vector>
    
    //自定义数据类型
    class My_String{
    public:
        My_String(std::string ss):s(ss){
            std::cout<<"有参构造"<<std::endl;
        }
        My_String(const My_String& ms){
            s = ms.s;
            std::cout<<"拷贝构造"<<std::endl;
        }
        My_String(My_String&& ms) noexcept{//如果不加noexcept,push_back时会采用拷贝构造的方式,加了就使用移动构造的方式。
            s = std::move(ms.s);
            std::cout<<"移动构造"<<std::endl;
        }
    private:
        std::string s;
    };
    
    class A{
    public:
        void fun(My_String ms){
            v.push_back(std::move(ms));
        }
    private:
        std::vector<My_String> v;
    };
    
    int main()
    {
        A a;
        a.fun(My_String("yy"));//有参 移动
    
        My_String ms("xx");//有参
        a.fun(ms);//拷贝 移动 移动
        return 0;
    }
    

总之:对于参数可能传入左值或右值的情况。使用重载或万能引用时,发生1次拷贝或移动;使用按值传递时,会额外产生一次移动操作。

  1. 对于只移型别,不考虑按值传递:因为根本不需要提供 左值版本 的 函数
  2. 一定发生复制,可能发生移动时,如果移动操作成本低,才考虑使用按值传递:因为此时额外的一次移动操作成本不大,换来的是不需要重载函数、避免了万能引用模板函数的缺陷
  3. 按值传递派生类对象 会导致 切片问题
    即:总是使用重载/万能引用的方式
  • 实现std::string
class My_String{
public:
    My_String(const char* s){
        //1.手动遍历获取字符串长度
        const char* p = s;
        size_t len = 0;
        while (*p != '\0')
        {
            //保存数据
            _buffer._local[len] = *p;
            len++;
            p++;
        }
        _size = len;
        //2.容量:初始化为字符串长度
        _capacity = _size;
        //3.保存数据
        if (_size <= 15)
        {
            _buffer._local[len+1] = '\0';
        }else{
            _buffer._pointer = const_cast<char*>(s);
        }
    }
private:
    union Buffer{
        char* _pointer;
        char _local[16];
    };
    Buffer _buffer;
    size_t _size;
    size_t _capacity;
};

面向切面编程

如果继承体系中的很多无关联的对象 有一些公共行为,可以将这些非核心逻辑 横切出来
实现AOP的方式

  • 静态织入:AspectC++库,使用专门的语法和工具 在编译期 织入有关方面的代码
  • 动态织入:使用 动态代理 在运行期 对方法进行拦截
    #include <iostream>
    #include <memory>
    //父类
    class A{
    public:
        A(){}
        virtual ~A(){}
        virtual void core_fun(){}
    };
    //子类
    class A_son: public A{
    public:
        A_son(){};
        ~A_son(){};
        void core_fun(){
            std::cout<<"核心逻辑"<<std::endl;
        }
    };
    //抽离出来的一个切面
    class A_Proxy:public A{
    public:
        A_Proxy(A* a):a(a){};
        ~A_Proxy(){
            delete a;
            a = nullptr;
        };
        void core_fun(){
            std::cout<<"非核心代码1"<<std::endl;
            a->core_fun();
            std::cout<<"非核心代码2"<<std::endl;
        }
    private:
        A* a;
    };
    int main() {
        A* a = new A_Proxy(new A_son);
        a->core_fun();
        return 0;
    }
    

置入操作 emplace_back

v.push_back(“abc”):"abc"调用string构造函数 生成 临时对象tmp,传递给vector的push_back右值引用版本,内存中为右值引用形参创建副本 用于作为移动的源。
v.emplace_back(“abc”):避免临时对象的创建和析构

  • 什么时候使用emplace_back
    1. 想添加的值是 以构造的方式 加入容器的,则使用置入。往已有对象位置赋值,则使用插入
    2. 实参类型和容器持有类型不一样时,使用置入。因为类型一样时不会创建临时对象
    3. 容器允许元素重复(或大部分值唯一)时 使用置入,否则使用插入。因为容器实现不重复的方式是 使用新值创建临时对象,与容器进行对比后,临时对象释放。

    即:类型不同、构造方式、允许重复 使用 emplace_back

  • 10
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值