2023-4-6-C++11、C++14、C++17、C++20版本新特性系统全面的学习!(一)

简介: 2023-4-6-C++11、C++14、C++17、C++20版本新特性系统全面的学习!

😉一、C++历史版本编年史

年份 C++标准 通用名
1978 C with Classes -
1998 ISO/IEC 14882:1998 C++98
2003 ISO/IEC 14882:2003 C++03
2011 ISO/IEC 14882:2011 C++11
2014 ISO/IEC 14882:2014 C++14
2017 ISO/IEC 14882:2017 C++17
2020 ISO/IEC 14882:2020 C++20
2023 waiting waiting

😆小知识

什么是C++0X:上一个版本的C++国际标准是2003年发布的,所以叫C++ 03。然后C++国际标准委员会在研究C++ 03的下一个版本的时候,一开始计划是07年发布,所以最初这个标准叫C++ 07。但是到06年的时候,官方觉得07年肯定完不成C++ 07,而且官方觉得08年可能也完不成。最后干脆叫C++ 0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的时候也没完成,最后在2011年终于完成了C++标准。所以最终定名为C++11。

😆小知识

C++的简单由来:

1998年是C++标准委员会成立的第一年,以后每5年视实际需要更新一次标准。

2009年,C++标准有了一次更新,一般称该草案为C++0x。

C++0x是C++11标准成为正式标准之前的草案临时名字。

后来,2011年,C++新标准标准正式通过,更名为ISO/IEC 14882:2011,简称C++11。

😆小知识

C++11后的进化史:

  • C++11,先前被称作C++0x,即ISO/IEC 14882:2011,是C++编程语言的一个标准。
    它取代第二版标准ISO/IEC 14882:2003 (第一版ISO/IEC 14882:1998公开于1998年, 第二版于2003年更新,分别通称C++98以及C++03,两者差异很小),且已被C++14取代
  • C++14 旨在作为C++11的一个小扩展,主要提供漏洞修复和小的改进。
    2014年8月18日,经过C++标准委员投票,C++14标准获得一致通过。ISO/IEC 14882:2014
  • C++17 又称C++1z,是继 C++14 之后,C++ 编程语言 ISO/IEC 标准的下一次修订的非正式名称。
    官方名称 ISO/IEC 14882:2017,基于 C++ 11,C++ 17 旨在简化该语言的日常使用,使开发者可以更简单地编写和维护代码。
    -C++20在2020年更新,用C++之父的话说C++20目的就是stability–稳定(稳定压到一切)和
    evolution–更新(与时俱进,走到潮流之巅)

🎂二、C++11新特性

auto自动类型推导

  • auto:让编译器在编译器就推导出变量的类型,可以通过=右边的类型推导出变量的类型,类似于C#之中的var。可以用它来躲开一堆长长的类型名,比如STL容器的iterator。
  • auto的自动类型推断发生在编译期,所以使用auto并不会造成程序运行时效率的降低。而是否会造成编译期的时间消耗,我认为是不会的,在未使用auto时,编译器也需要得知右操作数的类型,再与左操作数的类型进行比较,检查是否可以发生相应的转化,是否需要进行隐式类型转换。
  • 使用auto会让你的代码变得容易维护,上边例子中,for循环里所有变量的类型都是由b推导出来了。如果有人突然觉得这里int太小了,要改为unsigned long呢?如果你像上边那样写,你需要做的事情无非就是把int改为unsigned long——其他变量的类型变化就交给编译器和auto来处理好了i会自动变为unsigned long类型。
#include "iostream"
#include "vector"
auto func() { //函数的返回值可以用auto自动推导
    auto b = std::vector<int>{1, 2, 3, 4};
    for (auto i: b) {//auto还可以这么用
        std::cout << i << std::endl;
    }
}
int main() {
    auto a = 10;//无需显式的声明int a=10,编译器在运行的时候会自动推导。
    return 0;
};

注意事项:

  • auto 变量必须在定义时初始化,这类似于const关键字。
int main() {
 auto a=0;
 auto b;//不进行初始化会报错
};
  • 定义在一个auto序列的变量必须始终推导成同一类型。
int main() {
 auto a=10,b=10.0;//一个是int一个是非int类型,无法正确推导
};
  • 如果初始化表达式是引用,则去除引用语义。
#include "iostream"
#include "vector"
int main() {
    int a = 10;
    int &b = a;
    auto c = b;
    auto &d = b;
    c = 100;
    std::cout << a << std::endl;//输出为100
    d = 1000;
    std::cout << a << std::endl;//输出为1000
    std::cout << typeid(c).name() << std::endl;//输出为int
    std::cout << typeid(d).name() << std::endl;//输出为int
    std::cout << &a << std::endl;//输出为0000003225CFFCB4
    std::cout << &b << std::endl;//输出为0000003225CFFCB4
    std::cout << &c << std::endl;//输出为0000003225CFFCD4
    std::cout << &d << std::endl;//输出为0000003225CFFCB4
};
  • 如果初始化表达式为const或volatile(或者两者兼有),则除去const/volatile语义。
#include "iostream"
#include "vector"
int main() {
    const int a1 = 10;
    auto  b1= a1; //b1的类型为int而非const int(去除const)
    const auto c1 = a1;//此时c1的类型为const int
    b1 = 100;//合法
    c1 = 100;//非法
};
  • 如果auto关键字带上&号,则不去除const语意。
#include "iostream"
#include "vector"
int main() {
    const int a2 = 10;
    auto &b2 = a2;//因为auto带上&,故不去除const,b2类型为const int
    std::cout<< typeid(a2).name()<<std::endl;//int 为const int
    std::cout<< typeid(b2).name()<<std::endl;//int 为const int
    b2=1;//非法
};
  • 初始化表达式为数组时,auto关键字推导类型为指针。若表达式为数组且auto带上&,则推导类型为数组类型。
#include "iostream"
#include "vector"
int main() {
    int a[3] = {1, 2, 3};
    auto b = a;
    auto &c = a;
    std::cout << typeid(a).name() << std::endl;//int [3]
    std::cout << typeid(b).name() << std::endl;//int *__ptr64
    std::cout << typeid(c).name() << std::endl;//int [3]
};
  • 函数或者模板参数不能被声明为auto
void func(auto a)  //错误
{
    //... 
}
  • 时刻要注意auto并不是一个真正的类型。
    auto仅仅是一个占位符,它并不是一个真正的类型,不能使用一些以类型为操作数的操作符,如sizeof或者typeid。
cout << sizeof(auto) << endl;//错误
    cout << typeid(auto).name() << endl;//错误

decltype

decltype则用于推导表达式类型,这里只用于编译器分析表达式的类型,表达式实际不会进行运算,decltype不会像auto一样忽略引用和cv属性,decltype会保留表达式的引用和cv属性

对于decltype(exp)有

  • exp是表达式,decltype(exp)和exp类型相同
  • exp是函数调用,decltype(exp)和函数返回值类型相同
  • 其它情况,若exp是左值,decltype(exp)是exp类型的左值引用
#include "iostream"
#include "vector"
#include "functional"
void *fun() {
    return nullptr;
}
int main() {
    int a = 3;
    decltype(a) b = 0;
    decltype(fun()) c;//
    decltype(a + b) d = 0;//d是int,因为(a+b)返回一个右值
    decltype(a += b) e = d;//e是int&,因为(a+=b)返回一个左值
    std::cout << typeid(a).name() << std::endl;//int
    std::cout << typeid(b).name() << std::endl;//int
    std::cout << typeid(c).name() << std::endl;//void * __ptr64
    std::cout << typeid(d).name() << std::endl;//int
    std::cout << typeid(e).name() << std::endl;//int
    std::cout << d << std::endl;//0
    e = 5;
    std::cout << d << std::endl;//5 因为e是int&
};

😆小知识

decltype如何配合auto使用

auto和decltype一般配合使用在推导函数返回值的类型问题上,

例如

template decltype(t + u) add(T t, U u) { // t和u尚未定义 return t + u; }

这段代码在C++11上是编译不过的,因为在decltype(t +u)推导时,t和u尚未定义,就会编译出错,所以有了下面的叫做返回类型后置的配合使用方法:

template auto add(T t, U u) -> decltype(t + u) { return t + u; }

返回值后置类型语法就是为了解决函数返回制类型依赖于参数但却难以确定返回值类型的问题。

编译器自动判断了返回值类型,牛啊!

左值右值

这个知识点请跳转我另一篇文章,讲的非常的清楚明白

😚2023-3-9-一篇简短的文章把C++左右值关系讲的透透彻彻

列表初始化

C++11新增了列表初始化的概念。

在C++11中可以直接在变量名后面加上初始化列表来进行对象的初始化。

#include "iostream"
#include "vector"
#include "functional"
int main() {
    std::vector a {1, 2, 3, 4, 5, 6};
};

列表初始化的一些规则:

首先说下聚合类型可以进行直接列表初始化,这里需要了解什么是聚合类型:

  1. 类型是一个普通数组,如int[5],char[],double[]等
  2. 类型是一个类,且满足以下条件:
    没有用户声明的构造函数
    没有用户提供的构造函数(允许显示预置或弃置的构造函数)
    没有私有或保护的非静态数据成员
    没有基类
    没有虚函数
    没有{}和=直接初始化的非静态数据成员
    没有默认成员初始化器
#include "iostream"
#include "vector"
#include "functional"
class A{
public:
    int a;//如果为private则错误
};
int main() {
    A a{0};
};
struct A {
    int a;
    int b;
    int c;
    A(int, int){}
};
int main() {
    A a{1, 2, 3};// error,A有自定义的构造函数,不能列表初始化
}
struct A {
    int a;
    int b;
    virtual void func() {} // 含有虚函数,不是聚合类
};
struct Base {};
struct B : public Base { // 有基类,不是聚合类
      int a;
    int b;
};
struct C {
    int a;
    int b = 10; // 有等号初始化,不是聚合类
};
struct D {
    int a;
    int b;
private:
    int c; // 含有私有的非静态数据成员,不是聚合类
};
struct E {
      int a;
    int b;
    E() : a(0), b(0) {} // 含有默认成员初始化器,不是聚合类
};

上面列举了一些不是聚合类的例子,对于一个聚合类型,使用列表初始化相当于对其中的每个元素分别赋值;对于非聚合类型,需要先自定义一个对应的构造函数,此时列表初始化将调用相应的构造函数。

😆小知识

我们平时开发使用STL过程中可能发现它的初始化列表可以是任意长度,大家有没有想过它是怎么实现的呢,答案是std::initializer_list,看下面这段示例代码:

struct CustomVec { std::vector data; CustomVec(std::initializer_list list) { for (auto iter = list.begin(); iter != list.end(); ++iter) { data.push_back(*iter); } } };

std::initializer_list,它可以接收任意长度的初始化列表,但是里面必须是相同类型T,或者都可以转换为T。

std::function

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

😆小知识

什么是可调用对象

满足以下条件之一就可称为可调用对象:

是一个函数指针

是一个具有operator()成员函数的类对象(传说中的仿函数),lambda表达式

是一个可被转换为函数指针的类对象

是一个类成员(函数)指针

bind表达式或其它函数对象

#include "iostream"
#include "vector"
#include "functional"
class A {
public:
    A(int a) {}
    void print_a() {
        std::cout << a << std::endl;
    }
public:
    int a = 4;
};
void print_num(int i) {
    std::cout << i << std::endl;
}
int main() {
    //存储自由函数
    std::function<void(int)> function = print_num;
    function(1);//输出1
    //存储lambda
    std::function<void(int)> function1 = [](int i) {
        std::cout << i << std::endl;
    };
    function1(2);//输出2
    //存储到 std::bind 调用的结果
    std::function < void() > function2 = std::bind(print_num, 3);
    function2();//输出3 如果function2为void(int)类型,那么该函数传的参数不会影响结果,实际的参数是bind绑定的参数
    //存储到成员函数的调用
    std::function<void(A)> function3 = &A::print_a;
    function3(A{4});//输出4
};

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

std::bind

使用std::bind可以将可调用对象和参数一起绑定,最主要的功能就是实现延迟调用

  • 将可调用对象与参数一起绑定为另一个std::function供调用
  • 将n元可调用对象转成m(m < n)元可调用对象,绑定一部分参数,这里需要使用std::placeholders
#include "iostream"
#include "vector"
#include "functional"
#include <memory>
void print_num(int i,int k,int j) {
    std::cout << i << std::endl;
}
int main() {
    //存储自由函数
    std::function<void(int,int)> function = std::bind(print_num,5,std::placeholders::_1,std::placeholders::_2);
    function(2,3);//输出1
};

lambda表达式

lambda表达式可以说是c++11引用的最重要的特性之一,它定义了一个匿名函数,可以捕获一定范围的变量在函数内部使用,一般有如下语法形式:

auto func = [capture] (params) opt -> ret { func_body; };

其中func是可以当作lambda表达式的名字,作为一个函数使用,capture是捕获列表,params是参数表,opt是函数选项(mutable之类), ret是返回值类型,func_body是函数体。

一个完整的lambda表达式:

auto func1 = [](int a) -> int { return a + 1; };
auto func2 = [](int a) { return a + 2; };
cout << func1(1) << " " << func2(2) << endl;

如上代码,很多时候lambda表达式返回值是很明显的,c++11允许省略表达式的返回值定义。

lambda表达式允许捕获一定范围内的变量:

  • []不捕获任何变量
  • [&]引用捕获,捕获外部作用域所有变量,在函数体内当作引用使用
  • [=]值捕获,捕获外部作用域所有变量,在函数内内有个副本使用
  • [=, &a]值捕获外部作用域所有变量,按引用捕获a变量
  • [a]只值捕获a变量,不捕获其它变量
  • [this]捕获当前类中的this指针
#include "iostream"
#include "vector"
#include "functional"
int main() {
    //值捕获
    int a = 0;
    std::function < void() > function = [=]() -> void {
        a=10;//报错
        std::cout << a << std::endl;//0
    };
    function();
    std::cout << a << std::endl;//0
    //引用捕获
    std::function < void() > function1 = [&]() -> void {
        a=10;
        std::cout << a << std::endl;//10
    };
    function1();
    std::cout << a << std::endl;//10
};

代码中的a=10是编译不过的,因为我们修改了按值捕获的外部变量,其实lambda表达式就相当于是一个仿函数,仿函数是一个有operator()成员函数的类对象,这个operator()默认是const的,所以不能修改成员变量,而加了mutable,就是去掉const属性。

#include "iostream"
#include "vector"
#include "functional"
int main() {
    //值捕获加mutable 
    int a = 0;
    std::function < void() > function = [=]() mutable -> void {
        a=10;//此时不报错
        std::cout << a << std::endl;//10
    };
    function();
    std::cout << a << std::endl;//0
};

模板的改进

  • 模板的右尖括号
    C++11之前是不允许两个右尖括号出现的,会被认为是右移操作符,所以需要中间加个空格进行分割,避免发生编译错误。
  • 模板的别名
    C++11引入了using,可以轻松的定义别名,而不是使用繁琐的typedef。
typedef std::vector<std::vector<int>> vvi; // before c++11
using vvi = std::vector<std::vector<int>>; // c++11
  • 函数模板的默认模板参数
    C++11之前只有类模板支持默认模板参数,函数模板是不支持默认模板参数的,C++11后都支持。

并发

c++11引入了std::thread来创建线程,支持对线程join或者detach

#include "iostream"
#include "thread"
#include "memory"
#include "functional"
int main() {
    std::function < void() > func = []() {
        for (int i = 0; i < 10; i++) {
            std::cout << i << " ";
        }
        std::cout << std::endl;
    };
    std::thread t(func);
    if(t.joinable())
    {
        t.detach();//使用detach()函数会让线程在后台运行,即说明主线程不会等待子线程运行结束才结束通常称分离线程为守护线程(daemon threads),UNIX中守护线程是指,没有任何显式的用户接口,并在后台运行的线程。这种线程的特点就是长时间运行;线程的生命周期可能会从某一个应用起始到结束,可能会在后台监视文件系统,还有可能对缓存进行清理,亦或对数据结构进行优化。另一方面,分离线程的另一方面只能确定线程什么时候结束,发后即忘(fire andforget)的任务就使用到线程的这种方式
    }
    std::function < void(int) > func1 = [](int k) {
        for (int i = 0; i < k; i++) {
            std::cout << i << " ";
        }
        std::cout << std::endl;
    };
    std::thread tt(func1, 20);
    if(tt.joinable())
    {
        tt.join();//join()函数是一个等待线程完成函数,主线程需要等待子线程运行结束了才可以结束
    }
    return 0;
}

std::mutex是一种线程同步的手段,用于保存多线程同时操作的共享数据。

mutex分为四种:

  • std::mutex:独占的互斥量,不能递归使用,不带超时功能
  • std::recursive_mutex:递归互斥量,可重入,不带超时功能
  • std::timed_mutex:带超时的互斥量,不能递归
  • std::recursive_timed_mutex:带超时的互斥量,可以递归使用
#include "iostream"
#include "thread"
#include "mutex"
#include "functional"
std::timed_mutex mutex;
int main() {
    std::function < void(int) > func1 = [](int k) {
        mutex.try_lock_for(std::chrono::microseconds (1));
        for (int i = 0; i < k; i++) {
            std::cout << i << " ";
        }
        std::cout << std::endl;
        mutex.unlock();
    };
    std::vector<std::thread> threads;
    for(int i=0;i<5;i++)
    {
        threads.emplace_back(std::thread(func1,200));
    }
    for(auto& i:threads)
    {
        if(i.joinable())
        {
            i.join();
        }
    }
    return 0;
}
//因为只加了一微秒的锁,锁一解开就开始出现线程抢占资源的现象
//0 1 2 3 4 5 6 7 8 9 10 11 120 13 14  115  216 3 17 4  518 6 19 7 20 21  228  923 24 25 26 27 28 29 30 31 32 33 34 35 36 370 1  380 39  1 2 3 4  510 60  11140  7 122 2 3  48   9 5 31013 11 14   1512 441  426  743   8 44 5 
//4513 14  15 4616 17 47  18 1948 20  4921 50 51  52 22 23 24916 10  1117  18 19 20  2125  22 626 7 27 8 912 10 53  54 5523 24   2528 26   272911 3013 12  1413 15 14 1656 17  57  5831 1815 19  20  2128   3259  221629 17 30 18  31 1933 20    2334 3235 33 60 36  3461 213524 36 25 37   38 26 39 27 40  284122 29   30 31 3237 33  34 35 36  3742 3823 39 2462 25  63 26 6443 38 65 44  4566 46  674039 41  4047 48 49 50  5168 52 69  5327 54 28   55  2942 30 43 44 31 32  334541  4670 4742  48  49  5034 51 35 3656 37  5738   5839 5971 60  6172 62 7343 44  45 4046 41  47 42   4348 7452 75  5376 77 54 55 63  6444   4549  5056  65 57 66 58 5946 4751 48  52 4978 50  5179  526067 61  68   6953 70  5362 54 55 56  5463 5580 56  5781  5782 58   597158 72   7364 74 75 65 7683  77 60  7866 79  6784 68 59 69  60 70 61 71 72 6261 63  646285 6380 64 73  7465  75  8681  82  876683 88   8984 7667 77 65 78 90 79  80  8166  8285 83 84 85 86  8786 88 8991 90 91 92  6892 69   937093 94  9567  876896 69  8870 89 90  91 92  93 9471 95  72 9671  9472 95 73   7497  9798   9899  9973100  101 100102 101 103   104 105102 106 103 107  75108  109 110 11176 112  113 7774 114 115104 116  117105  78106118 107 119  10896 97  9879   9912075 12180 122  123  12481 12576   12677 127   12882 129 78  13079  131100 132 83 133 84101 85  10286   87109 110  111 112 113103 10480 105  106 81107  108134 109   135 11482 115  83 13688 137  138 13989 140 141  14284 143  144110 145 146   111147116 148 85 8690 87  88  8991 11290 113117 114   115 116   1179192 92  93149 94 95 150 96  151118118 97  93119  11994 120 98120   121 99152  10015395 154  155  15696  121 122 123  124 125 157101 122 102  12310397 104 98  105  126106 127  107  108124158 125  159126 99160  100128 101 102  103  104129  130105 131 161132 162 109133 110  111106  127163 128 164  165  166129 167134   168112 113135 114 136  137115  116 117 107   169138 170  171 172118 173130 174 131   132139 133 140  134 119175 120 176  177 178121 179 122 180 123 181141 182  183108 109  110 184142 185  186 143124  144 125111 126 112   113 127145 128187  146  188129147 189 148   190114  191 192135 193130 194 131 195149 132   133196 134   135136115 137  138150 151  152 153 154 155  156 197  198139116 140  141 142117 143 118 119157 158  159 199136  137 
// 120160 121 161  162 122144 138 145 139 123 146  163124 164 125 126 165  //140147166  127148 128  149 129  130167 131 150132  133151 134 152 135 153 136 137  154141   168142  169 143 170 144 155 138 156 139171 140  141145 142  143146  144 147157 172 145 158   146148 149159 150 151 160 173161 162  163174 164 165 166  167  152  153175168  169  170 147171 148154  149172 155176 156   157177  178 150179 180 151 158152  153 159154  160173  174181  175161  162176  177  178 155163 156 164   157165179 166182 167  168  169158   159183 160  161 184180 185  186170 171162 172  173163181 164  165182  183 166 174187 175 176  177167    168178188 169  179184 189 185  190 186170   180191 181171 182 187172 188   192173 183 193 184 194 185 195  186 189196174 190  197   187175198  188 199 189191  190 176
//191  192192 193  193194   177195 178  194179 195 180 196196 197 198  181199  182
//  183197  184198  185199  186
// 187 188 189 190 191 192 193 194 195 196 197 198 199 

std::lock相关

可以动态的释放锁资源,防止线程由于编码失误导致一直持有锁,c++11主要有std::lock_guard和std::unique_lock两种方式,使用方式都类似。

😆小知识

lock_guard 类是一个mutex封装者,它为了拥有一个或多个mutex而提供了一种方便的 RAII style 机制。( 译注:所谓的RAII,全称为Resource Acquisition Is Initialization,汉语是“资源获取即初始化”。但是这个直译并没有很好地解释这个词组的含义。其实含义并不高深复杂,简单说来就是,在资源获取的时候将其封装在某类的object中,利用"栈资源会在相应object的生命周期结束时自动销毁"来自动释放资源,即,将资源释放写在析构函数中。所以这个RAII其实就是和智能指针的实现是类似的。)。

当一个lock_guard对象被创建后,它就会尝试去获得给到它的mutex的所有权。当控制权不在该lock_guard对象所被创建的那个范围后,该lock_guard就会被析构,从而mutex被释放。

定义lock_guard的时候调用构造函数加锁,大括号解锁的时候调用析构函数解锁。虽然lock_guard挺好用的,但是有个很大的缺陷,在定义lock_guard的地方会调用构造函数加锁,在离开定义域的话lock_guard就会被销毁,调用析构函数解锁。这就产生了一个问题,如果这个定义域范围很大的话,那么锁的粒度就很大,很大程序上会影响效率。因此就引入了unique_lock

😆小知识

unique_lock<> unique();

这个会在构造函数加锁,然后可以利用unique.unlock()来解锁,所以当你觉得锁的粒度太多的时候,可以利用这个来解锁,而析构的时候会判断当前锁的状态来决定是否解锁,如果当前状态已经是解锁状态了,那么就不会再次解锁,而如果当前状态是加锁状态,就会自动调用unique.unlock()来解锁。而lock_guard在析构的时候一定会解锁,也没有中途解锁的功能。当然,方便肯定是有代价的,unique_lock内部会维护一个锁的状态,所以在效率上肯定会比lock_guard慢。

#include "iostream"
#include "thread"
#include "mutex"
#include "functional"
std::timed_mutex mutex;
int main() {
    std::function < void(int) > func1 = [](int k) {
        std::lock_guard<std::timed_mutex> lock(mutex);
        for (int i = 0; i < k; i++) {
            std::cout << i << " ";
        }
        lock.unlock();//报错,不支持中途解锁,会导致锁的粒度太大,可以使用unique_lock
        std::cout << std::endl;
    };
    return 0;
}

std::atomic相关

c++11提供了原子类型std::atomic,理论上这个T可以是任意类型

#include "iostream"
#include "thread"
#include "mutex"
#include "functional"
struct NewCounter { // 使用原子变量的计数器
    std::atomic<int> count;
    void add() {
        ++count;
        // count.store(++count);这种方式也可以
    }
    void sub() {
        --count;
        // count.store(--count);
    }
    int get() {
        return count.load();
    }
};
int main() {
    return 0;
}

std::call_once相关

c++11提供了std::call_once来保证某一函数在多线程环境中只调用一次,它需要配合std::once_flag使用

#include "iostream"
#include "thread"
#include "mutex"
#include "functional"
std::once_flag onceFlag;
int main() {
    std::function<void()> func1 = []() {
        std::call_once(onceFlag, []() {
            std::cout << "call once" << std::endl;
        });
    };
    std::thread threads[5];
    for (int i = 0; i < 5; ++i) {
        threads[i] = std::thread(func1);
    }
    for (auto &th: threads) {
        th.join();
    }
    return 0;
}

std::condition_variable相关

条件变量是c++11引入的一种同步机制,它可以阻塞一个线程或者个线程,直到有线程通知或者超时才会唤醒正在阻塞的线程,条件变量需要和锁配合使用,这里的锁就是上面介绍的std::unique_lock。

#include <iostream>                // std::cout
#include <thread>                // std::thread
#include <mutex>                // std::mutex, std::unique_lock
#include <condition_variable>    // std::condition_variable
std::mutex mtx; // 全局互斥锁.
std::condition_variable cv; // 全局条件变量.
bool ready = false; // 全局标志位.
void do_print_id(int id) {
    std::unique_lock<std::mutex> lck(mtx);
    cv.wait(lck, []{ return ready; });//一定要这么写,用while还是会出现虚假唤醒,不知道为啥
    // 线程被唤醒, 继续往下执行打印线程编号id.
    std::cout << "thread " << id << '\n';
}
void go() {
    std::unique_lock<std::mutex> lck(mtx);
    ready = true; // 设置全局标志位为 true.
    cv.notify_one(); // 唤醒所有线程.
}
void vecthread() {
    std::thread threads[10];
    // spawn 10 threads:
    for (int i = 0; i < 10; ++i)
        threads[i] = std::thread(do_print_id, i);
    for (auto &th: threads) th.detach();
}
int main() {
    std::thread create_vecthread(vecthread);
    create_vecthread.join(); //因为join了以后必定先执行
    std::cout << "10 threads ready to race...\n";
    go(); // go!
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return 0;
}

std::future相关

c++11关于异步操作提供了future相关的类,主要有std::future、std::promise和std::packaged_task,std::future比std::thread高级些,std::future作为异步结果的传输通道,通过get()可以很方便的获取线程函数的返回值,std::promise用来包装一个值,将数据和future绑定起来,而std::packaged_task则用来包装一个调用对象,将函数和future绑定起来,方便异步调用。而std::future是不可以复制的,如果需要复制放到容器中可以使用std::shared_future。

😆小知识

三者之间的关系

std::future用于访问异步操作的结果,而std::promise和std::packaged_task在future高一层,它们内部都有一个future,promise包装的是一个值,packaged_task包装的是一个函数,当需要获取线程中的某个值,可以使用std::promise,当需要获取线程函数返回值,可以使用std::packaged_task。

#include <iostream>
#include <functional>
#include <thread>
#include <future>     // std::promise, std::future
void print_int(std::future<int>& fut) {
    int x = fut.get();                    // 获取共享状态的值.
    std::cout << "value: " << x << '\n';  // 打印 value: 10.
}
int main ()
{
    std::promise<int> prom;                    // 生成一个 std::promise<int> 对象.
    std::future<int> fut = prom.get_future();  // 和 future 关联.
    std::thread t(print_int, std::ref(fut));   // 将 future 交给另外一个线程t.
    prom.set_value(10);                        // 设置共享状态的值, 此处和线程t保持同步.
    t.join();
    return 0;
}
#include <functional>
#include <future>
#include <iostream>
#include <thread>
using namespace std;
int func(int in) {
    return in + 1;
}
int main() {
    std::packaged_task<int(int)> task(func);
    std::future<int> fut = task.get_future();
    std::thread(std::move(task), 5).detach();
    cout << "result " << fut.get() << endl;
    return 0;
}

async相关

async是比future,packaged_task,promise更高级的东西,它是基于任务的异步操作,通过async可以直接创建异步的任务,返回的结果会保存在future中,不需要像packaged_task和promise那么麻烦,关于线程操作应该优先使用async

async具体语法如下:

async(std::launch::async | std::launch::deferred, func, args...);

功能:第二个参数接收一个可调用对象(仿函数、lambda表达式、类成员函数、普通函数…)作为参数,并且异步或是同步执行他们。

对于是异步执行还是同步执行,由第一个参数的执行策略决定:

(1)、std::launch::async 传递的可调用对象异步执行;

(2)、std::launch::deferred 传递的可调用对象同步执行;

(3)、std::launch::async | std::launch::deferred 可以异步或是同步,取决于操作系统,我们无法控制;

(4)、如果我们不指定策略,则相当于(3)。

对于执行结果:

我们可以使用get、wait、wait_for、wait_until等待执行结束,区别是get可以获得执行的结果。如果选择异步执行策略,调用get时,如果异步执行没有结束,get会阻塞当前调用线程,直到异步执行结束并获得结果,如果异步执行已经结束,不等待获取执行结果;如果选择同步执行策略,只有当调用get函数时,同步调用才真正执行,这也被称为函数调用被延迟。

#include <functional>
#include <future>
#include <iostream>
#include <thread>
#include "time.h"
#include "chrono"
using namespace std;
int func(int in) {
    this_thread::sleep_for(chrono::microseconds(1000));
    return in + 1;
}
int main() {
    std::future<int> res = std::async(func, 5);
    std::future_status status;
    do {
        status = res.wait_for(std::chrono::microseconds(0));
        switch (status)
        {
            case std::future_status::ready:
                std::cout << "Ready..." << std::endl;
                //获取结果
                std::cout << res.get() << std::endl;
                break;
            case std::future_status::timeout:
                std::cout << "timeout..." << std::endl;
                break;
            case std::future_status::deferred:
                std::cout << "deferred..." << std::endl;
                break;
            default:
                break;
        }
    } while (status != std::future_status::ready);
    cout << res.get() << endl; // 这里会报错,使用std::shared_future不会报错
    return 0;
}

std::future与std::shard_future的用途都是为了占位,但是两者有些许差别。std::future的get()成员函数是转移数据所有权;std::shared_future的get()成员函数是复制数据。 因此: future对象的get()只能调用一次;无法实现多个线程等待同一个异步线程,一旦其中一个线程获取了异步线程的返回值,其他线程就无法再次获取。 std::shared_future对象的get()可以调用多次;可以实现多个线程等待同一个异步线程,每个线程都可以获取异步线程的返回值。



目录
相关文章
|
10天前
|
编译器 C++ 开发者
C++一分钟之-C++20新特性:模块化编程
【6月更文挑战第27天】C++20引入模块化编程,缓解`#include`带来的编译时间长和头文件管理难题。模块由接口(`.cppm`)和实现(`.cpp`)组成,使用`import`导入。常见问题包括兼容性、设计不当、暴露私有细节和编译器支持。避免这些问题需分阶段迁移、合理设计、明确接口和关注编译器更新。示例展示了模块定义和使用,提升代码组织和维护性。随着编译器支持加强,模块化将成为C++标准的关键特性。
27 3
|
16天前
|
编译器 C语言 C++
C++一分钟之-C++11新特性:初始化列表
【6月更文挑战第21天】C++11的初始化列表增强语言表现力,简化对象构造,特别是在处理容器和数组时。它允许直接初始化成员变量,提升代码清晰度和性能。使用时要注意无默认构造函数可能导致编译错误,成员初始化顺序应与声明顺序一致,且在重载构造函数时避免歧义。利用编译器警告能帮助避免陷阱。初始化列表是高效编程的关键,但需谨慎使用。
25 2
|
4天前
|
数据安全/隐私保护 C++
|
11天前
|
安全 JavaScript 前端开发
C++一分钟之-C++17特性:结构化绑定
【6月更文挑战第26天】C++17引入了结构化绑定,简化了从聚合类型如`std::tuple`、`std::array`和自定义结构体中解构数据。它允许直接将复合数据类型的元素绑定到单独变量,提高代码可读性。例如,可以从`std::tuple`中直接解构并绑定到变量,无需`std::get`。结构化绑定适用于处理`std::tuple`、`std::pair`,自定义结构体,甚至在范围for循环中解构容器元素。注意,绑定顺序必须与元素顺序匹配,考虑是否使用`const`和`&`,以及谨慎处理匿名类型。通过实例展示了如何解构嵌套结构体和元组,结构化绑定提升了代码的简洁性和效率。
23 5
|
15天前
|
Linux vr&ar C语言
Linux怎样更新Centos下Gcc版本支持C17?Centos7快速安装gcc8.3.1 可支持C++17(附gcc相关链接整理)
Linux怎样更新Centos下Gcc版本支持C17?Centos7快速安装gcc8.3.1 可支持C++17(附gcc相关链接整理)
54 2
|
4天前
|
存储 安全 编译器
|
4天前
|
编译器 C++
【C++】string类的使用④(字符串操作String operations )
这篇博客探讨了C++ STL中`std::string`的几个关键操作,如`c_str()`和`data()`,它们分别返回指向字符串的const char*指针,前者保证以&#39;\0&#39;结尾,后者不保证。`get_allocator()`返回内存分配器,通常不直接使用。`copy()`函数用于将字符串部分复制到字符数组,不添加&#39;\0&#39;。`find()`和`rfind()`用于向前和向后搜索子串或字符。`npos`是string类中的一个常量,表示找不到匹配项时的返回值。博客通过实例展示了这些函数的用法。
|
4天前
|
存储 C++
【C++】string类的使用③(非成员函数重载Non-member function overloads)
这篇文章探讨了C++中`std::string`的`replace`和`swap`函数以及非成员函数重载。`replace`提供了多种方式替换字符串中的部分内容,包括使用字符串、子串、字符、字符数组和填充字符。`swap`函数用于交换两个`string`对象的内容,成员函数版本效率更高。非成员函数重载包括`operator+`实现字符串连接,关系运算符(如`==`, `&lt;`等)用于比较字符串,以及`swap`非成员函数。此外,还介绍了`getline`函数,用于按指定分隔符从输入流中读取字符串。文章强调了非成员函数在特定情况下的作用,并给出了多个示例代码。
|
4天前
|
C++
【C++】string类的使用④(常量成员Member constants)
C++ `std::string` 的 `find_first_of`, `find_last_of`, `find_first_not_of`, `find_last_not_of` 函数分别用于从不同方向查找目标字符或子串。它们都返回匹配位置,未找到则返回 `npos`。`substr` 用于提取子字符串,`compare` 则提供更灵活的字符串比较。`npos` 是一个表示最大值的常量,用于标记未找到匹配的情况。示例代码展示了这些函数的实际应用,如替换元音、分割路径、查找非字母字符等。
|
4天前
|
C++
C++】string类的使用③(修改器Modifiers)
这篇博客探讨了C++ STL中`string`类的修改器和非成员函数重载。文章介绍了`operator+=`用于在字符串末尾追加内容,并展示了不同重载形式。`append`函数提供了更多追加选项,包括子串、字符数组、单个字符等。`push_back`和`pop_back`分别用于在末尾添加和移除一个字符。`assign`用于替换字符串内容,而`insert`允许在任意位置插入字符串或字符。最后,`erase`函数用于删除字符串中的部分内容。每个函数都配以代码示例和说明。