2023-4-6-C++11、C++14、C++17、C++20版本新特性系统全面的学习!!!(全面准确,建议关注收藏订阅专栏)



🍿*★,°*:.☆( ̄▽ ̄)/$:*.°★* 🍿

💥💥💥欢迎来到🤞汤姆🤞的csdn博文💥💥💥
💟💟喜欢的朋友可以关注一下,下次更新不迷路💟💟
😆😆😆私聊获取个人订阅号哦,欢迎订阅共同学习😆😆😆
💖💖💖💖可以加入大家庭群聊,一起学习天天有福利💖💖💖💖





🍬本文摘要

在这里插入图片描述
对于想要成为一名资深C++工程师的我来说,对于C++历代版本更新的了解是不可或缺的,每一个版本增加的新特性都是C++这门语言进化的成果。如果你问我为什么喜欢C++这门语言,我可以告诉你的是,C++这门语言它虽然不完美,有很多让人诟病的缺点,但是它一直在成长,一直在进化,一直在变得越来越好,在保持自己的优点的前提下努力的学习其它语言的优点,争取让自己变得越来越好,就好像一个刚刚踏入编程生涯的小白程序员,只有通过不断地学习才能一点一点逐渐的成长,变得更加的健壮,所以,我爱C++因为它如此的真实。这篇文章我会花大量的时间和巨长的篇幅带来一份尽可能详尽的系统的C++历代版本的演变和新特性的介绍,希望能够让读者读完后有所收获,欢迎点赞评论收藏加订阅呀。


目录



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

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

😆小知识
什么是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<typename T, typename U> decltype(t + u) add(T t, U u) { // t和u尚未定义 return t + u; }
这段代码在C++11上是编译不过的,因为在decltype(t +u)推导时,t和u尚未定义,就会编译出错,所以有了下面的叫做返回类型后置的配合使用方法:
template<typename T, typename U> 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<int> data; CustomVec(std::initializer_list<int> 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()可以调用多次;可以实现多个线程等待同一个异步线程,每个线程都可以获取异步线程的返回值。

智能指针

c++11引入了三种智能指针:

  • std::shared_ptr
  • std::weak_ptr
  • std::unique_ptr

shared_ptr
shared_ptr使用了引用计数,每一个shared_ptr的拷贝都指向相同的内存,每次拷贝都会触发引用计数+1,每次生命周期结束析构的时候引用计数-1,在最后一个shared_ptr析构的时候,内存才会释放。
智能指针还可以自定义删除器,在引用计数为0的时候自动调用删除器来释放对象的内存,代码如下:

std::shared_ptr<int> ptr(new int, [](int *p){ delete p; });

weak_ptr
weak_ptr是用来监视shared_ptr的生命周期,它不管理shared_ptr内部的指针,它的拷贝的析构都不会影响引用计数,纯粹是作为一个旁观者监视shared_ptr中管理的资源是否存在,可以用来返回this指针和解决循环引用问题。
unique_ptr
std::unique_ptr是一个独占型的智能指针,它不允许其它智能指针共享其内部指针,也不允许unique_ptr的拷贝和赋值。使用方法和shared_ptr类似,区别是不可以拷贝,unique_ptr也可以像shared_ptr一样自定义删除器,使用方法和shared_ptr相同。

#include <functional>
#include <iostream>
#include "chrono"

using namespace std;

class A {
public:
    A() {};

    ~A() {
        std::cout << "111" << std::endl;
    }
};
int main() {
    std::shared_ptr<A> sharedPtr = std::make_shared<A>();
    std::unique_ptr<A> uniquePtr = std::unique_ptr<A>(new A());
    std::shared_ptr<A> sharedPtr1(new A(), [](A *a) {
        std::cout << "wo" << std::endl;
    });//自定义删除器
    //输出wo 111 111 后构造的先析构
    return 0;
}

基于范围的for循环

vector<int> vec;

for (auto iter = vec.begin(); iter != vec.end(); iter++) { // before c++11
    cout << *iter << endl;
}

for (int i : vec) { // c++11基于范围的for循环
    cout << "i" << endl;
}

委托构造函数

委托构造函数允许在同一个类中一个构造函数调用另外一个构造函数,可以在变量初始化时简化操作,通过代码来感受下委托构造函数的妙处吧:

struct A {
    A(){}
    A(int a) { a_ = a; }

    A(int a, int b) : A(a) { b_ = b; }

    A(int a, int b, int c) : A(a, b) { c_ = c; }

    int a_;
    int b_;
    int c_;
};

继承构造函数

继承构造函数可以让派生类直接使用基类的构造函数,如果有一个派生类,我们希望派生类采用和基类一样的构造方式,可以直接使用基类的构造函数,而不是再重新写一遍构造函数,老规矩,看代码:

不使用继承构造函数:

struct Base {
    Base() {}
    Base(int a) { a_ = a; }

    Base(int a, int b) : Base(a) { b_ = b; }

    Base(int a, int b, int c) : Base(a, b) { c_ = c; }

    int a_;
    int b_;
    int c_;
};

struct Derived : Base {
    Derived() {}
    Derived(int a) : Base(a) {} // 好麻烦
    Derived(int a, int b) : Base(a, b) {} // 好麻烦
    Derived(int a, int b, int c) : Base(a, b, c) {} // 好麻烦
};
int main() {
    Derived a(1, 2, 3);
    return 0;
}

使用继承构造函数:

struct Base {
    Base() {}
    Base(int a) { a_ = a; }

    Base(int a, int b) : Base(a) { b_ = b; }

    Base(int a, int b, int c) : Base(a, b) { c_ = c; }

    int a_;
    int b_;
    int c_;
};

struct Derived : Base {
    using Base::Base;
};

int main() {
    Derived a(1, 2, 3);
    return 0;
}

只需要使用using Base::Base继承构造函数,就免去了很多重写代码的麻烦。

nullptr

nullptr是c++11用来表示空指针新引入的常量值,在c++中如果表示空指针语义时建议使用nullptr而不要使用NULL,因为NULL本质上是个int型的0,其实不是个指针。举例:

void func(void *ptr) {
    cout << "func ptr" << endl;
}

void func(int i) {
    cout << "func i" << endl;
}

int main() {
    func(NULL); // 编译失败,会产生二义性
    func(nullptr); // 输出func ptr
    return 0;
}

final

final用于修饰一个类,表示禁止该类进一步派生和虚函数的进一步重载。

struct Base final {
    virtual void func() {
        cout << "base" << endl;
    }
};

struct Derived : public Base{ // 编译失败,final修饰的类不可以被继承
    void func() override {
        cout << "derived" << endl;
    }

};

override

override用于修饰派生类中的成员函数,标明该函数重写了基类函数,如果一个函数声明了override但父类却没有这个虚函数,编译报错,使用override关键字可以避免开发者在重写基类函数时无意产生的错误。

struct Base {
    virtual void func() {
        cout << "base" << endl;
    }
};

struct Derived : public Base{
    void func() override { // 确保func被重写
        cout << "derived" << endl;
    }

    void fu() override { // error,基类没有fu(),不可以被重写
        
    }
};

default

c++11引入default特性,多数时候用于声明构造函数为默认构造函数,如果类中有了自定义的构造函数,编译器就不会隐式生成默认构造函数,如下代码:

struct A {
    int a;
    A(int i) { a = i; }
};

int main() {
    A a; // 编译出错
    return 0;
}

上面代码编译出错,因为没有匹配的构造函数,因为编译器没有生成默认构造函数,而通过default,程序员只需在函数声明后加上“=default;”,就可将该函数声明为 defaulted 函数,编译器将为显式声明的 defaulted 函数自动生成函数体,如下:

struct A {
    A() = default;
    int a;
    A(int i) { a = i; }
};

int main() {
    A a;
    return 0;
}

编译通过。

delete

c++中,如果开发人员没有定义特殊成员函数,那么编译器在需要特殊成员函数时候会隐式自动生成一个默认的特殊成员函数,例如拷贝构造函数或者拷贝赋值操作符,如下代码:

struct A {
    A() = default;
    int a;
    A(int i) { a = i; }
};

int main() {
    A a1;
    A a2 = a1;  // 正确,调用编译器隐式生成的默认拷贝构造函数
    A a3;
    a3 = a1;  // 正确,调用编译器隐式生成的默认拷贝赋值操作符
}

而我们有时候想禁止对象的拷贝与赋值,可以使用delete修饰,如下:
struct A {
A() = default;
A(const A&) = delete;
A& operator=(const A&) = delete;
int a;
A(int i) { a = i; }
};

int main() {
A a1;
A a2 = a1; // 错误,拷贝构造函数被禁用
A a3;
a3 = a1; // 错误,拷贝赋值操作符被禁用
}
delele函数在c++11中很常用,std::unique_ptr就是通过delete修饰来禁止对象的拷贝的。

explicit

explicit专用于修饰构造函数,表示只能显式构造,不可以被隐式转换,根据代码看explicit的作用:

#include <iostream>
using namespace std;

class Point {
public:
    int x, y;
    Point(int x = 0, int y = 0)
            : x(x), y(y) {}
};

void displayPoint(const Point& p)
{
    cout << "(" << p.x << ","
         << p.y << ")" << endl;
}

int main()
{
    displayPoint(1);//输出(1,0),y使用了默认值0
    Point p = 1;
}

我们定义了一个再简单不过的Point类, 它的构造函数使用了默认参数. 这时主函数里的两句话都会触发该构造函数的隐式调用. (如果构造函数不使用默认参数, 会在编译时报错)

显然, 函数displayPoint需要的是Point类型的参数, 而我们传入的是一个int, 这个程序却能成功运行, 就是因为这隐式调用. 另外说一句, 在对象刚刚定义时, 即使你使用的是赋值操作符=, 也是会调用构造函数, 而不是重载的operator=运算符.

这样悄悄发生的事情, 有时可以带来便利, 而有时却会带来意想不到的后果. explicit关键字用来避免这样的情况发生。

// 加了explicit之后的代码
#include <iostream>
using namespace std;

class Point {
public:
    int x, y;
    explicit Point(int x = 0, int y = 0)
            : x(x), y(y) {}
};

void displayPoint(const Point& p)
{
    cout << "(" << p.x << ","
         << p.y << ")" << endl;
}

int main()
{
    displayPoint(Point(1));
    Point p(1);
}

const

const字面意思为只读,可用于定义变量,表示变量是只读的,不可以更改,如果更改,编译期间就会报错。
主要用法如下:

  • 用于定义常量,const的修饰的变量不可更改。
const int value = 5;
  • 指针也可以使用const,这里有个小技巧,从右向左读,即可知道const究竟修饰的是指针还是指针所指向的内容。“左定值,右定向,const修饰不变量”。
char *const ptr; // 指针本身是常量
const char* ptr; // 指针指向的变量为常量
  • 在函数参数中使用const,一般会传递类对象时会传递一个const的引用或者指针,这样可以避免对象的拷贝,也可以防止对象被修改。
class A{};
void func(const A& a);
  • const修饰类的成员变量,表示是成员常量,不能被修改,可以在初始化列表中被赋值。
class A {
    const int value = 5;
};
class B {
    const int value;
    B(int v) : value(v){}
};
  • 修饰类成员函数,表示在该函数内不可以修改该类的成员变量。
class A{
    void func() const;
};
  • 修饰类对象,类对象只能调用该对象的const成员函数。
class A {
    void func() const;
};
const A a;
a.func();
  • const参数传递和函数返回值
  • const参数传递和函数返回值

constexpr

constexpr是c++11新引入的关键字,用于编译时的常量和常量函数,这里直接介绍constexpr和const的区别:

两者都代表可读,const只表示read only的语义,只保证了运行时不可以被修改,但它修饰的仍然有可能是个动态变量,而constexpr修饰的才是真正的常量,它会在编译期间就会被计算出来,整个运行过程中都不可以被改变,constexpr可以用于修饰函数,这个函数的返回值会尽可能在编译期间被计算出来当作一个常量,但是如果编译期间此函数不能被计算出来,那它就会当作一个普通函数被处理。如下代码:

#include<iostream>
using namespace std;

constexpr int func(int i) {
    return i + 1;
}

int main() {
    int i = 2;
    func(i);// 普通函数
    func(2);// 编译期间就会被计算出来
}

enum class

c++11新增有作用域的枚举类型
不带作用域的枚举代码:

enum AColor {
    kRed,
    kGreen,
    kBlue
};

enum BColor {
    kWhite,
    kBlack,
    kYellow
};

int main() {
    if (kRed == kWhite) {
        cout << "red == white" << endl;
    }
    return 0;
}

如上代码,不带作用域的枚举类型可以自动转换成整形,且不同的枚举可以相互比较,代码中的红色居然可以和白色比较,这都是潜在的难以调试的bug,而这种完全可以通过有作用域的枚举来规避。

有作用域的枚举代码:

enum class AColor {
    kRed,
    kGreen,
    kBlue
};

enum class BColor {
    kWhite,
    kBlack,
    kYellow
};

int main() {
    if (AColor::kRed == BColor::kWhite) { // 编译失败
        cout << "red == white" << endl;
    }
    return 0;
}

使用带有作用域的枚举类型后,对不同的枚举进行比较会导致编译失败,消除潜在bug,同时带作用域的枚举类型可以选择底层类型,默认是int,可以改成char等别的类型。

enum class AColor : char {
    kRed,
    kGreen,
    kBlue
};

我们平时编程过程中使用枚举,一定要使用有作用域的枚举取代传统的枚举。

非受限联合体

c++11之前union中数据成员的类型不允许有非POD类型,而这个限制在c++11被取消,允许数据成员类型有非POD类型,看代码:

struct A {
    int a;
    int *b;
};

union U {
    A a; // 非POD类型 c++11之前不可以这样定义联合体
    int b;
};
union Myunion{
    int a = 0 ;
    double b = 0.0;//报错  因为在union联合体中只能对一个变量进行定义
    char c ='a';//报错  同理
};

😆小知识
在这里顺便补充一下内存对齐的知识:
union(联合体)类型中的数据共用内存,联合的所有成员共用一段内存空间,存储地址的起始位置都相同,一般来说最大成员的内存宽度作为union的内存大小,还必须满足是所有成员的整数倍,主要的原因是为了节省内存空间,默认的访问权限是公有的,但是它同样要遵守内存对齐的原则。
一,内存对齐的三条规则

  • 数据成员对齐规则,结构体(struct)(或联合(union))的数据成员,第一个数据成员存放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员(只要该成员有子成员,比如数组、结构体等)大小的整数倍开始(如:int 在 64bit 目标平台下占用 4Byte,则要从4的整数倍地址开始存储)
  • 结构体作为成员,如果一个结构体里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储
  • 结构体的总大小,即sizeof的结果,必须是其内部最大成员长度(即前面内存对齐指令中提到的有效值)的整数倍,不足的要补齐

二,注意事项

  • 数组在内存中存储时是分开存储的,char类型的数组每个元素是 1Byte,内存对齐时按照单个元素进行对齐
  • C++中空结构体占用 1Byte
  • C++中空类占用 1Byte
  • 类和结构体一样,需要内存对齐


union Myunion {
    int a = 0;
    double b;
    char c;
};

struct Mystruct {
    int a = 0;
    double b;
    char c;
};

int main() {
    Myunion myunion;
    Mystruct mystruct;
    myunion.b = 2.0;
    std::cout << myunion.a << std::endl;//0
    std::cout << myunion.b << std::endl;//2
    std::cout << myunion.c << std::endl;//
    std::cout << "a " << &myunion.a << std::endl;//a 00000049666FF6B8
    std::cout << "b " << &myunion.b << std::endl;//b 00000049666FF6B8
    std::cout << "c " << &myunion.c << std::endl;//c
    std::cout << sizeof(myunion) << std::endl;//8  原因是因为内部占用空间最大的元素是double类型,占用八个字节
    std::cout << sizeof(mystruct) << std::endl;//24 原因是因为内部占用空间最大的元素是double类型,占用八个字节,一共三个元素,共3*8
    return 0;
}

注意下面这种情况,内存大小为48
在这里插入图片描述

struct Test {
    int a;
    double b;
    char c;
};
 
struct Test3 {
    int a;
    Test d;
    double b;
    char c;
};//48

😆小知识
字节对齐的原因

  • 平台原因(移植原因),不是所有的硬件平台都能任意访问地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
  • 性能原因,提高CPU内存访问速度,一般处理器的内存存取粒度都是N的整数倍,假如访问N大小的数据,没有进行内存对齐,有可能就需要两次访问才可以读取出数据,而进行内存对齐可以一次性把数据全部读取出来,提高效率。

sizeof

不需要定义一个对象,在计算对象的成员大小。

struct Mystruct {
    int a = 0;
    double b;
    char c;
};

int main() {
    //C++11之前只能通过Mystruct struct1; sizeof(struct1.b),现在可以通过下面这个方式使用sizeof
    std::cout << sizeof(Mystruct::b) << std::endl;//8
    return 0;
}

assertion

通常来说,断言并不是正常程序所必需的,但对于程序调试来说,通常断言能够帮助开发者快速定位那些违反了某些前提条件的程序错误。在C++中,头文件提供了assert宏,提供运行时断言。如下:

#include <cassert>
#include "iostream"

int main() {
    int a = 5;
    int b = 0;
    assert(b != 0);
    std::cout << a / b << std::endl;
    return 0;
}

上述代码中对除数使用了断言,当除数为0时程序会报错。

然而,前述的断言只能在运行时才会起作用,这意味着不运行程序我们不会知道程序是否有错,而且就算运行了也只有在的调用到assert相关的代码路径时才会检查出来,这再某些情况下是不可接受的。因此我们还需要(特别是在模板编程中)在编译时期就能产生断言的机制。为此,C++11推出了静态断言。

C++的静态断言语法很简单,也可以自定义错误提示信息:
static_assert(bool_constexpr, message) 从C++11起
使用静态断言,我们可以在编译期间发现更多的错误,用编译器来强制保证一些契约
断言(assertion)是编程中的一种常用手段,在通常情况下,断言就是将一个返回值总是真(或者我们需要是真)的判别式放在语句中,用以排除在设计逻辑上不应该出现的情况。举个例子:我们都知道除数不能为0,那么就可以对除数使用断言,以使程序在除数为0的情况下产生异常退出。

#include <cassert>
#include "iostream"

int main() {
    int a = 5;
    int b = 0;
    static_assert(b != 0);//编译的时候就提示错误,这里会显示红色的下波浪线
    std::cout << a / b << std::endl;
    return 0;
}

自定义字面量

C++11允许用户自定义字面量后缀

#include <cassert>
#include "iostream"
#include "thread"

constexpr long double operator "" _cm(long double x) {
    return x * 10;
}

constexpr long double operator "" _m(long double x) {
    return x * 1000;
}

constexpr int operator "" _mm(unsigned long long x) {
    return (int) x;
}


int main() {

    std::this_thread::sleep_for(std::chrono::microseconds(1000_mm));
    return 0;
}

如果希望在编译时就调用字面量后缀函数,则需要把函数定义为 constexpr

内存对齐

内存对齐的知识可以看上面非受限联合体里面的讲解,这里对上述的讲解进行补充扩展
std::aligned_storage可以看成一个内存对其的缓冲区
sizeof : 获取内存存储的大小。
alignof : 获取地址对其的大小,POD里面最大的内存对其的大小。

#include <cassert>
#include "iostream"
#include "thread"

class A {
    int a;
    char b;
};


int main() {

    //下面这个函数用于创建一块对齐的内存,
    static std::aligned_storage<sizeof(A),
            alignof(A)>::type data;
    std::cout << typeid(data).name() << std::endl;//union std::_Align_type<int,8>
    A *attr = new(&data)A;
    std::cout << typeid(attr).name() << std::endl;//class A * __ptr64
    A *attr2 = new A;
    std::cout << typeid(attr2).name() << std::endl;//class A * __ptr64
    return 0;
}

thread_local

c++11引入thread_local,用thread_local修饰的变量具有thread周期,每一个线程都拥有并只拥有一个该变量的独立实例,一般用于需要保证线程安全的函数中。

对于一个线程私有变量,一个线程拥有且只拥有一个该实例,类似于static。

基础数值类型

c++11新增了几种数据类型:long long、char16_t、char32_t等

char 类型是 C 和 C++ 中的原始字符类型。 char 类型可用于存储 ASCII 字符集或任何 ISO-8859 字符集中的字符,以及多字节字符的单个字节,例如 Shift-JIS 或 Unicode 字符集的 UTF-8 编码。 在 Microsoft 编译器中,char 是 8 位类型。 它是与 signed char 和 unsigned char 都不同的类型。 默认情况下,char 类型的变量将提升到 int,就像是从 signed char 类型一样,除非使用 /J 编译器选项。 在 /J 的情况下,它们被视为 unsigned char 类型并提升为 int (没有符号扩展)。

类型 unsigned char 通常用于表示 byte,它不是 C++ 中的内置类型。

wchar_t 类型是实现定义的宽字符类型。 在 Microsoft 编译器中,它表示一个 16 位宽字符,用于存储编码为 UTF-16LE 的 Unicode(Windows 操作系统上的本机字符类型)。 通用 C 运行时 (UCRT) 库函数的宽字符版本使用 wchar_t 及其指针和数组类型作为参数和返回值,本机 Windows API 的宽字符版本也是如此。

char8_t、char16_t 和 char32_t 类型分别表示 8 位、16 位和 32 位宽字符。 (char8_t 是 C++20 中的新增功能,需要 /std:c++20 或 /std:c++latest 编译器选项。)编码为 UTF-8 的 Unicode 可以存储在 char8_t 类型中。 char8_t 和 char 类型的字符串称为“窄”字符串,即使用于编码 Unicode 或多字节字符。 编码为 UTF-16 的 Unicode 可以存储在 char16_t 类型中,而编码为 UTF-32 的 Unicode 可以存储在 char32_t 类型中。 这些类型和 wchar_t 类型的字符串都称为“宽”字符串,但该术语通常特指 wchar_t 类型的字符串。

在 C++ 标准库中,basic_string 类型专用于窄字符串和宽字符串。 字符的类型为 char 时,使用 std::string;字符的类型为 char8_t 时,使用 std::u8string;字符的类型为 char16_t 时,使用 std::u16string;字符的类型为 char32_t 时,使用 std::u32string;而字符的类型为 wchar_t 时,使用 std::wstring。 其他表示文本的类型(包括 std::stringstream 和 std::cout)均可专用于窄字符串和宽字符串。

随机数功能

c++11关于随机数功能则较之前丰富了很多,典型的可以选择概率分布类型,先看如下代码:

#include <time.h>

#include <iostream>
#include <random>

using namespace std;

int main() {
    std::default_random_engine random(time(nullptr));

    std::uniform_int_distribution<int> int_dis(0, 100); // 整数均匀分布
    std::uniform_real_distribution<float> real_dis(0.0, 1.0); // 浮点数均匀分布

    for (int i = 0; i < 10; ++i) {
        cout << int_dis(random) << ' ';
    }
    cout << endl;

    for (int i = 0; i < 10; ++i) {
        cout << real_dis(random) << ' ';
    }
    cout << endl;

    return 0;
}

代码中举例的是整数均匀分布和浮点数均匀分布,c++11提供的概率分布类型还有好多,例如伯努利分布、正态分布等,具体可以见最后的参考资料。

正则表达式

正则表达式(Regular Expression,常简写为regex、regexp或RE)。正则表达式通常被用来检索、替换那些符合某个模式(规则)的文本。C++11开始支持正则表达式。
正则表达式非常强大,具体的实现算法有差异,所以会有多种实现方式。C++11支持6种正则表达式引擎。ECMAScript 是其中支持最多元素的引擎,也是regex默认支持的引擎。

  • ECMAScript
  • basic(POSIX Basic Regular Expressions)
  • extended(POSIX Extended Regular Expressions )
  • awk(POSIX awk)
  • grep(POSIX grep )
  • egrep(POSIX grep –E)

正则表达式主要两部分构成,特殊字符和普通字符。

字符描述
\转义字符
$匹配字符行尾
*匹配前面的子表达式任意多次
+匹配前面的子表达式一次或多次
匹配前面的子表达式零次或一次
{m}匹配确定的m次
{m,}匹配至少m次
{m,n}{m,n}
.匹配任意字符
xy
[xyz]字符集合,匹配包含的任意一个字符
[^xyz]匹配未包含的任意字符
[a-z]字符范围,匹配指定范围内的任意字符
[^a-z]匹配任何不在指定范围内的任意字符
字符描述
\w匹配字母或数字或下划线,任意一个字母或数字或下划线,即AZ,az,0~9,_中任意一个
\W匹配任意不是字母、数字、下划线的字符
\s匹配任意的空白符,包括空格、制表符、换页符等空白字符的其中任意一个,与”[ \f\n\r\t\v]”等效
\S匹配任意不是空白符的字符,与”[^\f\n\r\t\v]”等效
\d匹配数字,任意一个数字,0~9中的任意一个,等效于”[0-9]”
\D匹配任意非数字的字符,等效于”[^0-9]”
\b匹配一个字边界,即字与空格间的位置,也就是单词和空格之间的位置,不匹配任何字符,如,“er\b"匹配"never"中的"er”,但不匹配"verb"中的"er"
\B非字边界匹配,“er\B"匹配"verb"中的"er”,但不匹配"never"中的"er"
\f匹配一个换页符,等价于”\x0c”和”\cL”
\n匹配一个换行符,等价于”\x0a”和”\cJ”
\r匹配一个回车符,等价于”\x0d”和”\cM”
\t匹配一个制表符,等价于”\x09”和”\cI”
\v匹配一个垂直制表符,等价于”\x0b”和”\cK”
\cx匹配”x”指示的控制字符,如,\cM匹配Control-M或回车符,”x”的值必须在”A-Z”或”a-z”之间,如果不是这样,则假定c就是"c"字符本身

regex_match

全文匹配,即要求整个字符串符合匹配规则,返回true或false

#include "iostream"
#include "regex"

using namespace std;
int main() {
    std::string str="1111-1";
    cout<<std::regex_match(str,regex("\\d{4}-\\d{1,2}"))<<endl;//匹配”四个数字-一个或两个数字“,返回值为1匹配成功
    str="1320550505@qq.com";
    cout<<regex_match(str,regex("\\d{1,}@qq.com"))<<endl;//匹配”QQ邮箱“,返回值为1匹配成功
    return 0;
}

regex_search

搜索匹配,即搜索字符串中存在符合规则的子字符串。

#include "iostream"
#include "regex"

using namespace std;
int main() {
    string str= "hello2019-02-03word";
    smatch match;
    regex regex1("(\\d{4})-(\\d{1,2})-(\\d{1,2})");//搜索规则()表示把内容拿出来,不加()的话只有match[0],没有[1][2][3]
    std::cout<<regex_search(str,match,regex1);//找到了返回1
    std::cout<<match[0];//2019-02-03
    std::cout<<match[1];//2019
    std::cout<<match[2];//02
    std::cout<<match[3];//03

    return 0;
}
#include "iostream"
#include "regex"

using namespace std;

int main() {
    string str = "hello2019-02-03word,hello2019-02-03word,hello2019-02-03word    ";
    smatch match;
    regex regex1("(\\d{4})-(\\d{1,2})-(\\d{1,2})");//搜索规则()表示把内容拿出来,不加()的话只有match[0],没有[1][2][3]
    string::const_iterator citer = str.cbegin();
    while (regex_search(citer, str.cend(), match, regex1)) {
        citer = match[0].second;//将迭代器的指针指向匹配到的字符串后面的位置
        for (size_t i = 1; i < match.size(); i++) {
            cout << match[i] << " ";//把三个括号里面的内容拿出来
        }
        cout << endl;
    }
    return 0;
}

regex_replace
替换匹配,即可以将符合匹配规则的子字符串替换为其他字符串。

#include "iostream"
#include "regex"

using namespace std;

int main() {
    string str = "hello2019-02-03word,hello2019-02-03word,hello2019-02-03word";
    regex regex1("-");
    cout << regex_replace(str, regex1, "/") << endl;//hello2019/02/03word,hello2019/02/03word,hello2019/02/03word,函数的返回值就是代替之后的结果
    return 0;
}

chrono

关于chrono库该怎么用建议去我的另一篇专门写这个的文章看一看,有很多我自己敲的实例
2023-4-11-chrono库用法学习

新增数据结构

  • std::forward_list:单向链表,只可以前进,在特定场景下使用,相比于std::list节省了内存,提高了性能
std::forward_list<int> fl = {1, 2, 3, 4, 5};
for (const auto &elem : fl) {
    cout << elem;
}
  • std::unordered_set:基于hash表实现的set,内部不会排序,使用方法和set类似

  • std::unordered_map:基于hash表实现的map,内部不会排序,使用方法和map类似

  • std::array:数组,在越界访问时抛出异常,建议使用std::array替代普通的数组

    std::array是具有固定大小的数组。因此,它并不支持添加或删除元素等改变大小的操作。也就是说,当定义一个array时,除了指定元素类型,还要指定容器大小。

既然有了内置的数组,为什么还要引入array呢?
内置的数组有很多麻烦的地方,比如无法直接对象赋值,无法直接拷贝等等,同时内置的数组又有很多比较难理解的地方,比如数组名是数组的起始地址等等。相比较于如vector等容器的操作,内置数组确实有一些不方便的地方。因此,C++11就引入array容器来代替内置数组。
简单来说,std::array除了有内置数组支持随机访问、效率高、存储大小固定等特点外,还支持迭代器访问、获取容量、获得原始指针等高级功能。而且它还不会退化成指针给开发人员造成困惑。

使用array之前,需要包含头文件:

	# include <array>

定义array时,需要指定其数据类型和大小,两者不可或缺。同时,array的大小不能使用变量来指定,但对于内置数组来说,是可以使用变量来指定数组大小的。
定义array时,可以使用{}来直接初始化,也可以使用另外的array来构造,但不可以使用内置数组来构造。
std::array提供了[]、at、front、back、data的方式来进行元素:

访问方式含义
at访问指定的元素,同时进行越界检查
[]访问指定的元素
front访问第一个元素
backback
data返回指向内存中数组第一个元素的指针

和一般的容器一样,array还提供了迭代器的方式进行元素遍历和访问:

迭代器含义
begin返回指向容器第一个元素的迭代器
end返回指向容器尾端的迭代器
rbegin返回指向容器最后元素的逆向迭代器
rend返回指向前端的逆向迭代器

array支持其它一些函数:

函数含义
empty检查容器是否为空
size返回容纳的元素数
max_size返回可容纳的最大元素数
fill以指定值填充容器
swap交换内容
  • std::tuple:元组类型,类似pair,但比pair扩展性好
typedef std::tuple<int, double, int, double> Mytuple;
Mytuple t(0, 1, 2, 3);
std::cout << "0 " << std::get<0>(t);
std::cout << "1 " << std::get<1>(t);
std::cout << "2 " << std::get<2>(t);
std::cout << "3 " << std::get<3>(t);

新增算法

  • all_of:检测表达式是否对范围[first, last)中所有元素都返回true,如果都满足,则返回true
std::vector<int> v(10, 2);
if (std::all_of(v.cbegin(), v.cend(), [](int i) { return i % 2 == 0; })) {
  std::cout << "All numbers are even\n";
}
  • any_of:检测表达式是否对范围[first,
    last)中至少一个元素返回true,如果满足,则返回true,否则返回false,用法和上面一样
  • none_of:检测表达式是否对范围[first,
    last)中所有元素都不返回true,如果都不满足,则返回true,否则返回false,用法和上面一样
  • find_if_not:找到第一个不符合要求的元素迭代器,和find_if相反
  • find_if_not:找到第一个不符合要求的元素迭代器,和find_if相反
  • copy_if:复制满足条件的元素
  • itoa:对容器内的元素按序递增
  • minmax_element:返回容器内最大元素和最小元素位置
int main() {
    std::vector<int> v = {3, 9, 1, 4, 2, 5, 9};

    auto result = std::minmax_element(v.begin(), v.end());
    std::cout << "min element at: " << *(result.first) << '\n';
    std::cout << "max element at: " << *(result.second) << '\n';
    return 0;
}
// min element at: 1
// max element at: 9
  • is_sorted、is_sorted_until:返回容器内元素是否已经排好序。

🎂三、C++14新特性

函数返回值类型推导

C++14对函数返回类型推导规则做了优化,返回值也可推导,需要注意的几点

  • 返回值类型推导也可以用在模板中
  • 函数内如果有多个return语句,它们必须返回相同的类型,否则编译失败
  • 如果return语句返回初始化列表,返回值类型推导也会失败
  • 如果函数是虚函数,不能使用返回值类型推导
  • 返回类型推导可以用在前向声明中,但是在使用它们之前,翻译单元中必须能够得到函数定义
  • 回类型推导可以用在递归函数中,但是递归调用必须以至少一个返回语句作为先导,以便编译器推导出返回类型

lambda参数auto

在C++11中,lambda表达式参数需要使用具体的类型声明,在C++14中,对此进行优化,lambda表达式参数可以直接是auto:

#include "iostream"

int main()
{
    auto f = [](auto a){
        std::cout<<a<<std::endl;
    };
    f(5);//5
    f("tom");//tom

    return 0;
}

变量模板

C++14支持变量模板:

#include "iostream"

template<class T>
constexpr T pi = T(3.1415926);

int main() {
    std::cout << pi<int> << std::endl;//3
    std::cout << pi<double> << std::endl;//3.14
    return 0;
}

别名模板

C++14也支持别名模板:

#include "iostream"

template<class T,typename  K>
constexpr T pi = T(3.1415926*(K)1.5);

int main() {
    std::cout << pi<int,double> << std::endl;//4
    std::cout << pi<double,int> << std::endl;//3.14159
    return 0;
}

constexpr的限制

C++11中constexpr函数必须必须把所有东西都放在一个单独的return语句中,而14中constexpr则无此限制
C++11中constexpr函数可以使用递归,在C++14中可以使用局部变量和循环

#include "iostream"

constexpr int factorial(int n) { // C++11中不可,C++14中可以
    int ret = 0;
    for (int i = 0; i < n; ++i) {
        ret += i;
    }
    return ret;
}

int main() {
    std::cout << factorial(5) << std::endl;//10
    return 0;
}

[[deprecated]]标记

C++14中增加了deprecated标记,修饰类、变、函数等,当程序中使用到了被其修饰的代码时,编译时被产生警告,用户提示开发者该标记修饰的内容将来可能会被丢弃,尽量不要使用。

struct [[deprecated]] A { };

int main() {
    A a;
    return 0;
}

二进制字面量与整形字面量分隔符

C++14引入了二进制字面量,也引入了分隔符,防止看起来眼花哈~

#include "iostream"


int main() {
    int a = 111'111'1111;
    std::cout<<a;//1111111111
    return 0;
}

std::make_unique

我们都知道C++11中有std::make_shared,却没有std::make_unique,在C++14已经改善。格式如下

struct A {};
std::unique_ptr<A> ptr = std::make_unique<A>();

std::shared_timed_mutex与std::shared_lock

C++14通过std::shared_timed_mutex和std::shared_lock来实现读写锁,保证多个线程可以同时读,但是写线程必须独立运行,写操作不可以同时和读操作一起进行。

struct  ThreadSafe{
    mutable std::shared_timed_mutex mutex;
    int value = 0;

    int getValue() const {
        std::shared_lock<std::shared_timed_mutex> lock(mutex);
        return value;
    }

    void setValue(int value) {
        std::unique_lock<std::shared_timed_mutex> loc(mutex);
        ThreadSafe::value = value;
    }


};

std::integer_sequence

#include "iostream"
#include "mutex"
#include "shared_mutex"

template<class T,T... ints>
void func(std::integer_sequence<T,ints...> int_seq)
{
    std::cout<<int_seq.size();//7
}



int main() {
    func(std::integer_sequence<int, 9, 2, 5, 1, 9, 1, 6>{});
    return 0;
}

std::exchange

exchange内部实现如下,可以看见new_value的值给了obj,而没有对new_value赋值,这里相信您已经知道了它和swap的区别了吧!

template<class T, class U = T>
constexpr T exchange(T& obj, U&& new_value) {
    T old_value = std::move(obj);
    obj = std::forward<U>(new_value);
    return old_value;
}

swap是交换两个容器的值,exchange只是将一方的值给另一方

#include "iostream"
#include "mutex"
#include "shared_mutex"
#include "vector"

using namespace std;

int main() {
    std::vector<int> v = {1};
    std::vector<int> z = {1, 2};
    std::exchange(v, z);
    cout << v.size() << endl;//2
    cout << z.size() << endl;//2
    for (int a: v) {
        cout << a << " ";
    }
    v = {1};
    z = {1, 2};
    std::swap(v, z);
    cout << v.size() << endl;//2
    cout << z.size() << endl;//1
    for (int a: v) {
        cout << a << " ";
    }


    return 0;
}

std::quoted

C++14引入std::quoted用于给字符串添加双引号,直接看代码:

#include <iomanip>
#include "iostream"

using namespace std;

int main() {
    string str = "hello world";
    cout << str << endl;//hello world
    cout << std::quoted(str) << endl;//"hello world"
    return 0;
}

🎂四、C++17新特性

结构化绑定

通过结构化绑定,对于tuple、map等类型,获取相应值会方便很多

#include <iomanip>
#include "iostream"
#include "vector"
#include "tuple"

using namespace std;

auto fuc() {
    return tuple<int, double>(1, 2.0);
}

int main() {
    auto [i, d] = fuc();
    std::cout << i << " " << d << endl;
    return 0;
}

if-switch语句初始化

C++17之后可以这样:

#include <iomanip>
#include "iostream"
#include "vector"
#include "tuple"

using namespace std;
int main() {

    if (int i = 0;i <= 5) {
        std::cout << i << std::endl;//0
    }
    return 0;
}

内联变量

C++17前只有内联函数,现在有了内联变量,我们印象中C++类的静态成员变量在头文件中是不能初始化的,但是有了内联变量,就可以达到此目的:

#include <iomanip>
#include "iostream"
#include "vector"
#include "tuple"

using namespace std;

class tom {
public:
    inline static int a = 0;
};

int main() {
    std::cout << tom::a << std::endl;//0
    return 0;
}

折叠表达式

C++17引入了折叠表达式使可变参数模板编程更方便:

#include <iomanip>
#include "iostream"
#include "vector"
#include "tuple"

using namespace std;

template <typename ... Ts>
auto sum(Ts ... ts) {
    return (ts + ...);
}

int main() {


    int a {sum(1, 2, 3, 4, 5)}; // 15
    std::string c{"hello "};
    std::string b{"world"};
    cout << sum(c, b) << endl; // hello world

    return 0;
}

constexpr lambda表达式

C++17前lambda表达式只能在运行时使用,C++17引入了constexpr lambda表达式,可以用于在编译期进行计算。

#include <iomanip>
#include "iostream"
#include "vector"
#include "tuple"

using namespace std;

int main() {

    constexpr auto lamb = []() { std::cout << 1; };
    lamb();

    return 0;
}

😆小知识
constexpr函数有如下限制:
函数体不能包含汇编语句、goto语句、label、try块、静态变量、线程局部存储、没有初始化的普通变量,不能动态分配内存,不能有new delete等,不能虚函数。

namespace嵌套

namespace A {
    namespace B {
        namespace C {
            void func();
        }
    }
}

// c++17,更方便更舒适
namespace A::B::C {
    void func();)
}

__has_include预处理表达式

可以判断是否有某个头文件,代码可能会在不同编译器下工作,不同编译器的可用头文件有可能不同,所以可以使用此来判断:

#if defined __has_include
#if __has_include(<iostream>)
#define has_iostream 1
#include "iostream"
#endif
#endif

int main()
{
#ifdef has_iostream
    std::cout<<"yes";
#elif
    printf("no");
#endif
    return 0;
}

在lambda表达式用*this捕获对象副本

正常情况下,lambda表达式中访问类的对象成员变量需要捕获this,但是这里捕获的是this指针,指向的是对象的引用,正常情况下可能没问题,但是如果多线程情况下,函数的作用域超过了对象的作用域,对象已经被析构了,还访问了成员变量,就会有问题。

struct A {
    int a;
    void func() {
        auto f = [this] {
            cout << a << endl;
        };
        f();
    }  
};
int main() {
    A a;
    a.func();
    return 0;
}

所以C++17增加了新特性,捕获*this,不持有this指针,而是持有对象的拷贝,这样生命周期就与对象的生命周期不相关啦。

struct A {
    int a;
    void func() {
        auto f = [*this] { // 这里
            cout << a << endl;
        };
        f();
    }  
};
int main() {
    A a;
    a.func();
    return 0;
}

新增Attribute

C++17 引入了新的 Attribute 语法来给函数、变量、类型等添加元数据信息。Attribute 可以用于优化、调试、静态分析等方面。下面是一些示例:

  • [[nodiscard]]:用于指示一个函数的返回值应该被检查,如果没有使用返回值会产生编译器警告。
[[nodiscard]] int func() {
    return 0;
}

int main() {
    func(); // 编译器会提示忽略了未使用的返回值
    return 0;
}
  • [[fallthrough]]:用于标记 switch 语句中故意落入下一个 case 的情况。
switch (n) {
    case 1:
        do_something();
        [[fallthrough]];
    case 2:
        do_something_else();
        break;
    default:
        break;
}

  • [[maybe_unused]]:用于消除未使用变量的编译器警告。
void func([[maybe_unused]] int n) {
    // ...
}
  • [[deprecated]]:用于标记已经过时的函数或变量。
[[deprecated]] void old_func() {
    // ...
}
  • [[nodiscard(“message”)]]:用于指定警告消息,当返回值没有被使用时,输出指定的警告信息。
[[nodiscard("Please check the return value of this function.")]] int func() {
    return 0;
}

😆小知识
Attribute 不是标准 C++ 的一部分,而是 GCC 和 Clang 支持的扩展功能,因此不同编译器的支持程度可能会有所不同。

字符串转换

C++17 新特性中新增的 from_chars() 和 to_chars() 函数。

from_chars() 和 to_chars() 函数都是用于将数据类型转换为字符序列或者从字符序列转换为数据类型。其中,from_chars() 用于将字符序列解析为数值类型,并返回解析后的结果及指向未解析部分的指针;to_chars() 则相反,用于将数值类型转换为字符序列,并返回指向输出缓冲区尾部的指针。

#include <charconv>
#include <iostream>

int main() {
    std::string str = "123";
    int num;

    auto [p, ec] = std::from_chars(str.data(), str.data() + 1, num);

    if (ec == std::errc()) {
        std::cout << "The parsed number is: " << num << '\n';
        std::cout << p << '\n';//指向后一位指针
    } else
        std::cout << "Error parsing number\n";

    return 0;
}

用 to_chars() 函数将整型数值转换为字符序列

#include <charconv>
#include <iostream>

int main()
{
    int num = 123;
    char buf[20];

    auto [p, ec] = std::to_chars(buf, buf+sizeof(buf), num);

    if (ec == std::errc())
        std::cout << "The string representation of the number is: " << buf << '\n';
    else
        std::cout << "Error converting number\n";

    return 0;
}

std::variant

std::variant 是一个用于存储多个不同类型对象的类型,类似于 C++11 引入的 std::any,但 std::variant 只能存储预定义的一组类型(也就是 variant 的模板参数),而且它在编译期就已经确定了所包含的类型。与 std::any 不同,std::variant 仅支持对于自身包含的类型进行类型安全操作而避免了运行时开销,同时也更加灵活和易于使用。

下面是一个简单的示例,演示如何定义、初始化和访问 std::variant:

c++
#include <variant>
#include <iostream>
#include <string>

int main()
{
    std::variant<int, double, std::string> v; // 定义一个可存储 int、double 和 std::string 类型的 variant
    v = 3.1415926; // 将 double 值赋值给 variant
    double d = std::get<double>(v); // 使用 std::get 获取 variant 中的 double 值
    std::cout << "The value of v is: " << d << '\n';

    v = "Hello, world!"; // 将字符串赋值给 variant
    std::string s = std::get<std::string>(v); // 使用 std::get 获取 variant 中的字符串
    std::cout << "The value of v is: " << s << '\n';

    return 0;
}

在上面的示例中,我们首先定义了一个可存储 int、double 和 std::string 类型的 variant。然后,我们将一个 double 值赋值给 variant,并使用 std::get 获取 variant 中的 double 值。接着,我们将一个字符串赋值给 variant 并使用 std::get 获取 variant 中的字符串。

需要注意的是,std::get() 函数是一个重载函数,它的函数名和参数类型用于指定要访问的 variant 中的具体类型,而在访问时如果该类型并不存在,则会抛出 std::bad_variant_access 异常。

除了使用 std::get() 函数外,还可以使用 std::visit() 函数,来对 variant 中的对象进行操作,例如:

#include <variant>
#include <iostream>
#include <string>

struct PrintVisitor {
    void operator()(int i) const { std::cout << "The value of variant is: " << i << '\n'; }
    void operator()(double d) const { std::cout << "The value of variant is: " << d << '\n'; }
    void operator()(const std::string& s) const { std::cout << "The value of variant is: " << s << '\n'; }
};

int main()
{
    std::variant<int, double, std::string> v;
    v = 3.1415926;

    std::visit(PrintVisitor{}, v);

    return 0;
}

在上述示例中,我们定义了一个 PrintVisitor 结构体,并重载了它的 operator() 函数,以处理 variant 中的 int、double 和 std::string 类型的数据。然后,我们使用 std::visit() 函数来对 variant 中的对象进行操作,同时也实例化了 PrintVisitor 结构体。由于我们只需要访问 variant 中的 double 值,所以在使用 std::visit() 时,我们传入了一个 PrintVisitor 的实例和 variant 对象,这样就可以输出 variant 中的 double 值。

总之,std::variant 类型为 C++ 提供了一种类型安全且高效的多类型数据存储方式,既避免了运行时开销,又提高了代码的灵活性。

std::optional

std::optional 是一个用于表示可能不存在值的类型,类似于指针,但是它可以避免空指针引发的异常。当某个值可能不存在时,我们可以将其封装到 std::optional 对象中,以避免在访问该值时出现异常。

下面是一个简单的示例,演示如何定义、初始化和访问 std::optional:

#include <optional>
#include <iostream>

int main()
{
    std::optional<int> opt;
    if (opt.has_value()) { // 判断 optional 是否包含值
        int i = opt.value(); // 使用 value() 获取 optional 中的值
        std::cout << "The value of opt is: " << i << '\n';
    } else {
        std::cout << "opt does not have a value\n";
    }

    opt = 42; // 将一个值赋给 optional
    if (opt.has_value()) {
        int i = opt.value();
        std::cout << "The value of opt is: " << i << '\n';
    } else {
        std::cout << "opt does not have a value\n";
    }

    return 0;
}
#include <optional>
#include <iostream>

int main()
{
    std::optional<int*> opt;
    if (opt.has_value()) { // 判断 optional 是否包含值
        auto i = opt.value(); // 使用 value() 获取 optional 中的值
        std::cout << "The value of opt is: " << i << '\n';
    } else {
        std::cout << "opt does not have a value\n";
    }


    return 0;
}

在上面的示例中,我们首先定义了一个 std::optional 对象,并检查它是否包含值。由于该对象没有被初始化,因此不包含任何值。接着,我们将一个整数值赋给该对象,并再次检查它是否包含值。在检查时,我们使用了 has_value() 函数来判断 optional 是否包含值,并使用 value() 函数获取 optional 中的值。

需要注意的是,当 optional 对象未被初始化时,它并不包含任何值,此时访问其内容会抛出 std::bad_optional_access 异常。因此,在对 optional 进行访问之前,必须先确保它已经包含值。

除了使用 has_value() 和 value() 函数外,std::optional 还提供了一系列便捷的函数,如:

reset():将 optional 设置为无值状态
operator bool():将 optional 转换为 bool 值,true 表示 optional 包含值,false 表示无值
value_or(T&& default_value):获取 optional 中的值,如果 optional 为空,则返回提供的默认值
例如:

#include <optional>
#include <iostream>

int main()
{
    std::optional<int> opt;
    int i = opt.value_or(42); // 使用提供的默认值获取 optional 中的值
    std::cout << "The value of opt is: " << i << '\n';

    opt.reset(); // 将 optional 设置为空
    if (!opt) { // 使用 bool 转换函数判断 optional 是否为空
        std::cout << "opt does not have a value\n";
    }

    return 0;
}

上面的示例展示了通过 value_or() 函数获取默认值,以及使用 reset() 函数将 optional 对象设置为空的方法。

总之,std::optional 类型为 C++ 提供了一种安全、方便的方式来表示可能不存在值的情况,避免了在访问空指针时出现异常。由于 std::optional 实现了一些便捷的函数,因此使用起来非常方便。

std::any

是的,我可以为您介绍一下 C++17 新特性中新增的 std::any 类型及其使用方法。

std::any 是一个用于存储任意类型对象的类型,类似于 Python 中的 object 或者 Java 中的 Object。当我们需要存储不同类型的数据时,可以使用 std::any,在编译期间不需要知道所存储的具体类型,而在运行时再根据实际情况进行类型转换。

下面是一个简单的示例,演示如何定义、初始化和访问 std::any:

#include <any>
#include <iostream>
#include <string>

int main()
{
    std::any a;
    if (!a.has_value()) { // 判断 any 是否包含值
        std::cout << "a does not have a value\n";
    }

    a = 42; // 将一个整数值赋给 any
    if (a.has_value()) {
        int i = std::any_cast<int>(a); // 使用 any_cast 获取 any 中的值
        std::cout << "The value of a is: " << i << '\n';
    }

    a = std::string("Hello, world!"); // 将一个字符串赋给 any
    if (a.has_value()) {
        std::string s = std::any_cast<std::string>(a);
        std::cout << "The value of a is: " << s << '\n';
    }

    return 0;
}

在上面的示例中,我们首先定义了一个 std::any 对象,并检查它是否包含值。由于该对象没有被初始化,因此不包含任何值。接着,我们将一个整数值和一个字符串分别赋给该对象,并使用 any_cast() 函数获取 std::any 中的值。

需要注意的是,当 std::any 对象未被初始化时,它并不包含任何值,此时访问其内容会抛出 std::bad_any_cast 异常。因此,在对 std::any 进行访问之前,必须先确保它已经包含值。

除了使用 has_value() 和 any_cast() 函数外,std::any 还提供了一些便捷的函数,如:

  • reset():将 std::any 设置为空
  • type():获取 std::any 中存储的类型信息

例如:

#include <any>
#include <iostream>
#include <typeinfo>

int main()
{
    std::any a = 3.1415926;
    std::cout << "The type of a is: " << a.type().name() << '\n'; // 输出 a 中存储的类型信息

    a.reset(); // 将 a 设置为空
    if (!a.has_value()) {
        std::cout << "a does not have a value\n";
    }

    return 0;
}

上面的示例展示了通过 type() 函数获取 std::any 中所存储的类型信息,以及通过 reset() 函数将 std::any 设置为空的方法。

总之,std::any 类型为 C++ 提供了一种安全、方便的方式来存储任意类型的数据,避免了在存储不同类型的数据时需要使用多个变量或指针的麻烦。由于 std::any 实现了一些便捷的函数,因此使用起来非常方便。

std::apply

在 C++ 中,我们经常需要对一个 std::tuple 对象进行打包或解包操作。例如,当我们调用某个函数时,可能需要将一个 std::tuple 作为参数传递给该函数。而在函数内部,则需要对这个 std::tuple 进行解包,以获取其中的元素并进行处理。在 C++17 中,可以通过 std::apply 函数来方便地对 std::tuple 进行解包操作。

下面是一个简单的示例,演示如何使用 std::apply 来对 std::tuple 进行解包:

#include <iostream>
#include <tuple>

void print(int x, float y, double z)
{
    std::cout << "x = " << x << ", y = " << y << ", z = " << z << '\n';
}

int main()
{
    std::tuple<int, float, double> tpl(42, 3.14f, 2.71828);
    std::apply(print, tpl); // 对 tpl 进行解包,并将解包后的参数传递给 print 函数

    return 0;
}

在上面的示例中,我们定义了一个 std::tuple 对象 tpl,并将其作为参数传递给了 std::apply 函数和 print 函数。在 std::apply 函数内部,它会将 std::tuple 中的元素进行解包,并将解包后的参数传递给指定的函数。由于 print 函数需要三个参数,因此 std::tuple 中的元素也必须是三个。

需要注意的是,在使用 std::apply 函数时,被调用的函数必须支持参数展开。例如,如果被调用的函数只接受一个 std::tuple 对象作为参数,则无法使用 std::apply 对其进行解包操作。

除了 std::tuple 对象之外,std::apply 还可以对任意类型的可调用对象进行参数展开。例如:

#include <iostream>
#include <functional>

int add(int x, int y)
{
    return x + y;
}

int main()
{
    std::function<int(int, int)> f = add; // 将函数转换为 std::function 对象
    std::tuple<int, int> tpl(1, 2);
    int sum = std::apply(f, tpl); // 对 tpl 进行解包,并将解包后的参数传递给 f 函数
    std::cout << "The sum is: " << sum << '\n';

    return 0;
}

在上面的示例中,我们将一个普通的函数 add 转换为 std::function 对象,并将其作为参数传递给了 std::apply 函数。std::apply 函数会对 std::tuple 进行解包,并将解包后的参数传递给 std::function 对象。

总之,std::apply 函数为 C++ 提供了一种便捷的方式来对 std::tuple 或者其他可调用对象进行参数展开操作。它使得我们能够更加灵活地处理函数的参数,同时避免了参数个数过多或过少的问题。

std::make_from_tuple

在 C++ 中,我们经常需要使用 std::tuple 来存储一组数据。有时候,我们需要将这些数据传递给一个函数或者构造函数,并以某种方式生成一个新的对象。在 C++17 中,可以通过 std::make_from_tuple 函数来方便地从 std::tuple 对象中生成任意类型的对象。

下面是一个简单的示例,演示如何使用 std::make_from_tuple 函数来生成一个 std::pair 对象:

#include <iostream>
#include <utility>
#include <tuple>

int main()
{
    std::tuple<int, double> tpl(42, 3.14);
    auto p = std::make_from_tuple<std::pair<int, double>>(tpl); // 使用 std::make_from_tuple 生成 std::pair 对象
    std::cout << "first = " << p.first << ", second = " << p.second << '\n';

    return 0;
}

需要注意的是,在使用 std::make_from_tuple 函数时,被创建的对象必须支持从 std::tuple 中提取出对应的元素进行构造。由于 std::make_from_tuple 的实现依赖于 std::apply 函数,因此该函数仅在 C++17 及以上版本的标准库中可用。

总之,std::make_from_tuple 函数为 C++ 提供了一种便捷的方式来从 std::tuple 对象中生成任意类型的对象。它使得我们能够更加灵活地处理函数的参数,并避免了手动提取 std::tuple 元素的麻烦。

std::string_view

请跳转我的另一篇文章学习2023-2-19-什么是string_view

file_system

文件系统库 file_system 为 C++ 提供了一组用于访问计算机文件系统的工具函数和类。它包含了许多常见的文件操作功能,例如获取文件大小、读取目录内容、创建目录等。在 C++17 中,可以通过 #include 头文件来包含该库。

下面是一个简单的示例,演示如何使用 file_system 库来列出指定目录下的所有文件和子目录:

#include <iostream>
#include <filesystem>

namespace fs = std::filesystem;

int main()
{
    fs::path current_path = fs::current_path(); // 获取当前路径
    for (auto& entry : fs::directory_iterator(current_path)) { // 遍历当前路径下的所有文件和子目录
        if (entry.is_directory()) {
            std::cout << "[dir] ";
        } else {
            std::cout << "[file]";
        }
        std::cout << " " << entry.path() << '\n';
    }

    return 0;
}

在上面的示例中,我们使用 fs::path 类型的 current_path 对象来表示当前路径,并通过 fs::directory_iterator 类型的对象遍历该目录下的所有文件和子目录。对于每个文件或子目录,我们检查其类型并打印出相应的信息。

除了遍历目录之外,file_system 库还提供了许多其他实用的工具函数和类。例如,可以使用 fs::file_size 函数来获取文件的大小,使用 fs::create_directory 函数来创建新目录,使用 fs::exists 函数来检查文件或目录是否存在等等。

总之,文件系统库 file_system 提供了一组用于访问计算机文件系统的工具函数和类,可以帮助我们更加方便地进行文件操作。它是 C++17 的一个重要新增功能,在现代 C++ 开发中非常实用。

std::shared_mutex

std::shared_mutex 是一个读写锁(也称为共享-排他锁),允许多个线程同时读取某个共享资源,但只允许一个线程写入该资源。在 C++11 中,我们已经有了 std::mutex 和 std::lock_guard 等工具来实现互斥锁,但是它们都是排他锁,不能同时支持读取和写入操作。而在 C++17 中,我们可以使用 std::shared_mutex 类来实现读写锁。

下面是一个简单的示例,演示如何在多个线程中使用 std::shared_mutex 对共享变量进行读写操作:

#include <iostream>
#include <thread>
#include <mutex>

std::shared_mutex g_mutex;
int g_value = 0;

void read_value()
{
    std::shared_lock<std::shared_mutex> lock(g_mutex); // 获取读取锁
    std::cout << "The value is " << g_value << '\n';
}

void write_value()
{
    std::unique_lock<std::shared_mutex> lock(g_mutex); // 获取写入锁
    ++g_value;
}

int main()
{
    std::thread t1(read_value);
    std::thread t2(read_value);
    std::thread t3(write_value);

    t1.join();
    t2.join();
    t3.join();

    return 0;
}

在上面的示例中,我们定义了一个全局变量 g_value,并通过 std::shared_mutex 类型的 g_mutex 对其进行读写操作。在 read_value 函数中,我们通过 std::shared_lockstd::shared_mutex 获取读取锁,允许多个线程同时读取 g_value 变量的值。在 write_value 函数中,我们通过 std::unique_lockstd::shared_mutex 获取写入锁,保证只有一个线程可以对 g_value 变量进行写入操作。

需要注意的是,std::shared_mutex 类型的对象不能直接使用 std::lock_guard 进行加锁,而是需要使用 std::unique_lock 或 std::shared_lock 来获取锁。std::unique_lock 表示独占的锁,只允许一个线程获取,用于写入操作;std::shared_lock 表示共享的锁,允许多个线程同时获取,用于读取操作。

总之,std::shared_mutex 是 C++17 中新增的一个读写锁,用于实现多个线程对共享资源的读写操作。它是 C++ 并发编程中非常实用的工具,可以提高程序的并发性能和可靠性。


🎂五、C++20新特性

concept

C++20 引入了 Concept(概念)作为一种新的语言特性。Concept 主要用于约束模板参数,以帮助程序员定义和描述对类型的要求。接下来,我将为你提供一个简单的示例来说明 Concept 的用法。

我们假设有一个名为 Addable 的 Concept,该概念对可加性进行约束,即它只能应用于那些支持加法操作的类型。以下是一个示例代码:

#include <iostream>
#include <concepts>

template<typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::same_as<T>;
};

template<Addable T>
T add(T a, T b) {
    return a + b;
}

int main() {
    std::cout << add(3, 4) << std::endl; // 输出:7

    // 编译错误!字符串类型不符合 Addable 概念的要求
    // std::cout << add("Hello", "World") << std::endl;

    return 0;
}

在上述示例中,我们首先定义了一个 Concept Addable,它通过使用 requires 关键字来定义对类型 T 的要求。这里要求 T 类型支持加法运算,并且返回的结果类型与 T 相同。

然后,我们编写了一个模板函数 add,其模板参数 T 受限于 Addable 概念。这意味着该函数只能接受满足 Addable 概念要求的类型作为参数。

最后,在 main 函数中,我们演示了如何使用 add 函数来进行加法运算。我们成功地将整数 3 和 4 相加,并输出结果 7。然而,如果我们试图使用字符串调用 add 函数,编译过程将会失败,因为字符串类型不符合 Addable 概念的要求。

这个示例展示了 Concept 的基本用法,它可以帮助程序员对模板参数进行约束,提供更好的类型安全性和代码可读性。

requires

C++20 引入了 requires 关键字作为一种新的语言特性。requires 关键字主要用于在概念(Concept)中对类型或表达式进行要求约束。接下来,我将为您提供一个示例代码来说明 requires 的用法。

假设我们有一个 Printable 概念,它要求类型 T 必须具有一个名为 print 的成员函数,该函数将对象输出到标准输出。以下是一个示例代码:

#include <iostream>
#include <concepts>

template<typename T>
concept Printable = requires(T a) {
    { a.print() } -> std::same_as<void>;
};

struct MyType {
    void print() {
        std::cout << "Printing MyType" << std::endl;
    }
};

struct AnotherType {};

template<Printable T>
void printObject(T obj) {
    obj.print();
}

int main() {
    MyType myObj;
    printObject(myObj); // 输出:Printing MyType

    AnotherType anotherObj;
    // 编译错误!AnotherType 类型不符合 Printable 概念的要求
    // printObject(anotherObj);

    return 0;
}

在上述示例中,我们首先定义了一个 Concept Printable,它使用 requires 关键字来定义对类型 T 的要求。这里要求类型 T 必须有一个名为 print 的成员函数,并且返回类型是 void

然后,我们创建了一个名为 MyType 的结构体,它满足 Printable 概念的要求。该结构体拥有一个名为 print 的成员函数,它将字符串输出到标准输出。

接下来,我们定义了一个模板函数 printObject,其模板参数 T 受限于 Printable 概念。这意味着该函数只能接受满足 Printable 概念要求的类型作为参数,并调用其 print 成员函数进行输出操作。

最后,在 main 函数中,我们演示了如何使用 printObject 函数来输出对象。我们成功地将 MyType 类型的对象传递给 printObject 函数,并在控制台上输出了相应的信息。然而,如果我们尝试使用 AnotherType 类型的对象调用 printObject 函数,编译过程将会失败,因为 AnotherType 类型不符合 Printable 概念的要求。

这个示例展示了 requires 关键字的基本用法,它可以帮助程序员对概念进行约束和检查,以提高代码的可读性和可靠性。

constinit

C++20 引入了 constinit 关键字作为一种新的语言特性。constinit 主要用于声明具有静态存储期并且仅在编译时初始化的变量。接下来,我将为您提供一个示例代码来说明 constinit 的用法。

假设我们有一个名为 Vector 的结构体,它表示一个二维向量,并且我们想要确保该结构体的默认构造函数在编译时进行常量初始化。以下是一个示例代码:

#include <iostream>

struct Vector {
    double x;
    double y;

    constinit Vector() : x(0.0), y(0.0) {}
};

int main() {
    constinit Vector v; // 使用 constinit 声明需要编译时常量初始化的变量

    std::cout << "x: " << v.x << ", y: " << v.y << std::endl; // 输出:x: 0, y: 0

    return 0;
}

在上述示例中,我们定义了一个名为 Vector 的结构体,它具有两个成员变量 xy,分别表示二维向量的坐标。

在默认构造函数中,我们使用 constinit 关键字对 Vector 类型的对象进行了修饰。这意味着该对象必须在编译时进行常量初始化,并且不能在运行时修改。

main 函数中,我们创建了一个名为 vVector 类型的对象,并在控制台上输出了它的坐标值。由于我们使用了 constinit 关键字,编译器会在编译时对 v 进行常量初始化,并保证其在运行时不会被修改。

这个示例展示了 constinit 关键字的基本用法,它可以帮助程序员声明需要在编译时进行常量初始化的变量,并提供更好的性能和代码安全性。请注意,constinit 关键字在 C++20 中仍属于实验性特性,因此在使用时需谨慎考虑兼容性和标准遵循性。

consteval

C++20 引入了 consteval 关键字作为一种新的语言特性。consteval 主要用于声明一个只能在编译时求值的函数,它必须产生一个常量表达式结果。接下来,我将为您提供一个示例代码来说明 consteval 的用法。

假设我们想要实现一个用于计算斐波那契数列的函数,并且希望该函数在编译时就能够被求值。以下是一个示例代码:

#include <iostream>

consteval int fibonacci(int n) {
    if (n <= 1)
        return n;
    else
        return fibonacci(n - 1) + fibonacci(n - 2);
}

int main() {
    constexpr int result = fibonacci(5); // 使用 consteval 声明需要在编译时求值的函数

    std::cout << "Fibonacci(5): " << result << std::endl; // 输出:Fibonacci(5): 5

    return 0;
}

在上述示例中,我们定义了一个名为 fibonacci 的函数,它使用 consteval 关键字进行修饰。这意味着该函数只能在编译时求值,并且返回的结果必须是一个常量表达式。

在函数体内部,我们使用递归的方式计算斐波那契数列的第 n 个数,并返回结果。

main 函数中,我们使用 constexpr 关键字声明了一个常量 result,并调用 fibonacci 函数来计算斐波那契数列的第 5 个数。由于我们使用了 consteval 关键字,编译器会在编译时对函数进行求值,并将结果作为常量存储在 result 中。

最后,在控制台上输出了计算得到的斐波那契数列的结果。

这个示例展示了 consteval 关键字的基本用法,它可以帮助程序员声明只能在编译时求值的函数,并在需要进行编译时计算的场景中提供更好的性能和代码安全性。请注意,在使用 consteval 关键字时需要确保函数的实现是可以被编译器求值的常量表达式。

co_await

C++20 引入了 co_await 关键字作为一种新的语言特性。co_await 主要用于协程(coroutine)的实现,用于暂停当前协程并在稍后继续执行。接下来,我将为您提供一个示例代码来说明 co_await 的用法。

假设我们有一个异步任务,需要在某个时间点后返回结果。以下是一个简单的示例代码:

#include <iostream>
#include <chrono>
#include <experimental/coroutine>

struct AsyncTask {
    struct promise_type {
        int result;

        auto get_return_object() {
            return AsyncTask{std::experimental::coroutine_handle<promise_type>::from_promise(*this)};
        }

        auto initial_suspend() {
            return std::experimental::suspend_always{};
        }

        auto final_suspend() {
            return std::experimental::suspend_always{};
        }

        void return_value(int value) {
            result = value;
        }

        void unhandled_exception() {
            std::terminate();
        }
    };

    std::experimental::coroutine_handle<promise_type> handle;

    AsyncTask(std::experimental::coroutine_handle<promise_type> h) : handle(h) {}

    ~AsyncTask() {
        if (handle)
            handle.destroy();
    }

    bool await_ready() const {
        return false;
    }

    void await_suspend(std::experimental::coroutine_handle<>) const {}

    int await_resume() const {
        return handle.promise().result;
    }
};

AsyncTask performAsyncTask() {
    co_await std::chrono::seconds(3); // 模拟一个长时间运行的异步操作

    co_return 42; // 返回异步任务的结果
}

int main() {
    auto task = performAsyncTask();

    std::cout << "Performing async task..." << std::endl;

    int result = task.await_resume(); // 获取异步任务的结果

    std::cout << "Async task result: " << result << std::endl;

    return 0;
}

在上述示例中,我们首先定义了一个 AsyncTask 结构体,它代表一个异步任务。在该结构体中,我们实现了一个嵌套的 promise_type,用于管理异步任务的状态和结果。

get_return_object() 函数负责创建一个 AsyncTask 对象,并将其与协程句柄关联起来。

initial_suspend()final_suspend() 函数分别指定异步任务的初始暂停点和最终暂停点。

return_value() 函数用于设置异步任务的结果。

unhandled_exception() 函数处理异常情况。

接下来,我们实现了 performAsyncTask() 函数,该函数使用 co_await 关键字来暂停当前协程,并模拟一个耗时的异步操作。在等待时间过后,我们使用 co_return 关键字返回异步任务的结果。

main 函数中,我们创建了一个 task 对象,并调用 await_resume() 函数来获取异步任务的结果。

最后,在控制台上输出了异步任务的结果。

这个示例展示了 co_await 关键字的基本用法,它可以帮助程序员实现协程,并支持异步操作的简洁编写和灵活控制。请注意,协程相关的特性仍然处于实验性阶段,并且需要使用适当的编译器和标准库支持。

co_return

C++20 引入了 co_return 关键字作为一种新的语言特性。co_return 主要用于在协程中返回结果并终止协程的执行。接下来,我将为您提供一个示例代码来说明 co_return 的用法。

假设我们有一个异步任务,需要在某个时间点后返回结果。以下是一个简单的示例代码:

#include <iostream>
#include <chrono>
#include <experimental/coroutine>

struct AsyncTask {
    struct promise_type {
        int result;

        auto get_return_object() {
            return AsyncTask{std::experimental::coroutine_handle<promise_type>::from_promise(*this)};
        }

        auto initial_suspend() {
            return std::experimental::suspend_always{};
        }

        auto final_suspend() {
            return std::experimental::suspend_always{};
        }

        void return_value(int value) {
            result = value;
        }

        void unhandled_exception() {
            std::terminate();
        }
    };

    std::experimental::coroutine_handle<promise_type> handle;

    AsyncTask(std::experimental::coroutine_handle<promise_type> h) : handle(h) {}

    ~AsyncTask() {
        if (handle)
            handle.destroy();
    }

    bool await_ready() const {
        return false;
    }

    void await_suspend(std::experimental::coroutine_handle<>) const {}

    int await_resume() const {
        return handle.promise().result;
    }
};

AsyncTask performAsyncTask() {
    co_await std::chrono::seconds(3); // 模拟一个长时间运行的异步操作

    co_return 42; // 返回异步任务的结果并终止协程
}

int main() {
    auto task = performAsyncTask();

    std::cout << "Performing async task..." << std::endl;

    int result = task.await_resume(); // 获取异步任务的结果

    std::cout << "Async task result: " << result << std::endl;

    return 0;
}

在上述示例中,我们定义了一个 AsyncTask 结构体,它代表一个异步任务。在该结构体中,我们实现了一个嵌套的 promise_type,用于管理异步任务的状态和结果。

get_return_object() 函数负责创建一个 AsyncTask 对象,并将其与协程句柄关联起来。

initial_suspend()final_suspend() 函数分别指定异步任务的初始暂停点和最终暂停点。

return_value() 函数用于设置异步任务的结果。

unhandled_exception() 函数处理异常情况。

接下来,我们实现了 performAsyncTask() 函数,该函数使用 co_await 关键字来暂停当前协程,并模拟一个耗时的异步操作。在等待时间过后,我们使用 co_return 关键字返回异步任务的结果并终止协程的执行。

main 函数中,我们创建了一个 task 对象,并调用 await_resume() 函数来获取异步任务的结果。

最后,在控制台上输出了异步任务的结果。

这个示例展示了 co_return 关键字的基本用法,它可以帮助程序员实现协程,并支持异步操作的简洁编写和灵活控制。请注意,协程相关的特性仍然处于实验性阶段,并且需要使用适当的编译器和标准库支持。

co_yield

C++20 引入了 co_yield 关键字作为一种新的语言特性。co_yield 主要用于在协程中暂停执行并将值产生给调用方,然后再次恢复协程的执行。接下来,我将为您提供一个示例代码来说明 co_yield 的用法。

假设我们有一个异步任务,需要以递增顺序生成一系列数字,并将每个数字传递给调用方。以下是一个简单的示例代码:

#include <iostream>
#include <experimental/coroutine>

struct AsyncGenerator {
    struct promise_type {
        int current;

        auto get_return_object() {
            return AsyncGenerator{std::experimental::coroutine_handle<promise_type>::from_promise(*this)};
        }

        auto initial_suspend() {
            return std::experimental::suspend_always{};
        }

        auto final_suspend() {
            return std::experimental::suspend_always{};
        }

        void return_void() {}

        void unhandled_exception() {
            std::terminate();
        }

        auto yield_value(int value) {
            current = value;
            return std::experimental::suspend_always{};
        }
    };

    std::experimental::coroutine_handle<promise_type> handle;

    AsyncGenerator(std::experimental::coroutine_handle<promise_type> h) : handle(h) {}

    ~AsyncGenerator() {
        if (handle)
            handle.destroy();
    }

    bool move_next() {
        if (!handle.done()) {
            handle.resume();
            return true;
        }

        return false;
    }

    int current_value() const {
        return handle.promise().current;
    }
};

AsyncGenerator generateNumbers(int start, int end) {
    for (int i = start; i <= end; ++i) {
        co_yield i; // 暂停协程并将当前值产生给调用方
    }
}

int main() {
    auto generator = generateNumbers(1, 5);

    while (generator.move_next()) {
        std::cout << "Current value: " << generator.current_value() << std::endl;
    }

    return 0;
}

在上述示例中,我们首先定义了一个 AsyncGenerator 结构体,它代表一个异步生成器。在该结构体中,我们实现了一个嵌套的 promise_type,用于管理异步生成器的状态和值。

get_return_object() 函数负责创建一个 AsyncGenerator 对象,并将其与协程句柄关联起来。

initial_suspend()final_suspend() 函数分别指定异步生成器的初始暂停点和最终暂停点。

return_void() 函数用于处理 void 类型的返回值。

unhandled_exception() 函数处理异常情况。

yield_value() 函数接收一个值,并将该值设为当前值,然后暂停协程并将当前值产生给调用方。

接下来,我们实现了 generateNumbers() 函数,该函数使用 co_yield 关键字在循环中暂停协程,并以递增顺序产生数字。

main 函数中,我们创建了一个 generator 对象,并使用 move_next() 函数在每次迭代中继续协程的执行。通过 current_value() 函数获取当前生成器产生的值,并在控制台上输出。

最后,我们按顺序输出了生成器产生的数字。

这个示例展示了 co_yield 关键字的基本用法,它可以帮助程序员实现协程,并支持生成器模式的简洁编写和灵活控制。请注意,协程相关的特性仍然处于实验性阶段,并且需要使用适当的编译器和标准库支持。

char8_t

C++20 引入了 char8_t 类型作为一种新的字符类型,用于表示 UTF-8 编码的字符。以下是一个简单的示例代码来说明 char8_t 的用法:

#include <iostream>

int main() {
    const char8_t* utf8Str = u8"Hello, 世界!"; // 使用 char8_t 类型的字符串

    std::cout << "UTF-8 String: " << reinterpret_cast<const char*>(utf8Str) << std::endl;

    return 0;
}

在上述示例中,我们声明一个变量 utf8Str,它是一个指向 const char8_t 类型的字符串的指针。我们使用 u8 前缀将字符串字面量标记为 char8_t 类型。

通过 reinterpret_cast,我们将 utf8Str 转换为 const char* 类型,并通过 std::cout 输出到控制台。

请注意,在 C++20 中,char8_t 类型主要用于 UTF-8 字符串的处理和操作。与传统的 charwchar_t 不同,char8_t 类型只能用于存储和处理 UTF-8 编码的字符。

这个示例展示了如何使用 char8_t 类型来处理 UTF-8 编码的字符串。通过 char8_t 类型,我们可以更加准确地表示和操作 UTF-8 字符,以适应全球化和国际化的需求。

import

在C++20中,import是一项新的语言特性,它允许我们直接从模块导入定义,而不需要使用传统的头文件包含方式。这可以提供更清晰、更可靠的代码组织和模块化。

下面是一个简单的示例,说明如何使用import来导入一个模块并使用其中的定义:

// math_module.cppm
export module math_module;

export int add(int a, int b) {
    return a + b;
}

上面的代码定义了一个名为math_module的模块,并导出了一个add函数。

现在,我们可以创建另一个源文件来导入该模块并使用其中的定义:

// main.cpp
import math_module;

int main() {
    int result = add(5, 3);
    return 0;
}

在上面的示例中,我们使用import语句将math_module模块导入到main.cpp中。然后,我们可以直接使用add函数来执行加法运算。

需要注意的是,为了支持模块导入,编译器需要以模块文件(通常使用.cppm扩展名)的形式编译模块定义。因此,在上述示例中,math_module.cppm文件应该被编译为模块。

这只是import特性的基本用法示例,还有其他高级用法,例如命名空间导入和重命名导入等。然而,由于C++20的标准实现在目前尚未完全普及,这些高级用法可能在不同的编译器之间存在差异。请查阅你所使用的编译器的文档以获取更多详细信息。

module

在C++20中,module是一项新的语言特性,它用于定义和导出模块,以实现更好的代码组织和模块化。下面是一个简单的示例,说明如何使用module来定义和使用模块:

// math_module.cppm
export module math_module;

export int add(int a, int b) {
    return a + b;
}

上述代码创建了一个名为math_module的模块,并导出了一个add函数。

然后,我们可以创建另一个源文件来导入该模块并使用其中的定义:

// main.cpp
import math_module;

int main() {
    int result = add(5, 3);
    return 0;
}

在上面的示例中,我们使用import语句将math_module模块导入到main.cpp中。然后,我们可以直接使用add函数来执行加法运算。

需要注意的是,为了支持模块化,编译器需要将模块文件(通常使用.cppm扩展名)作为模块进行编译。因此,在上述示例中,math_module.cppm文件应该被编译为模块。

通过使用模块,我们可以避免传统头文件的包含方式,提供更清晰、更可靠的代码组织,并提升编译速度和可维护性。同时,模块也支持命名空间、重命名导入等高级用法,以满足更复杂的需求。

需要注意的是,由于C++20的标准实现在目前尚未完全普及,编译器对模块的支持可能存在差异。要使用模块特性,请确保你所使用的编译器已经支持C++20标准中的模块功能,并查阅相关文档以获取更多详细信息。

Lambda 表达式的更新

常量表达式(constexpr) 的更新

原子(Atomic)智能指针

常量表达式(constexpr) 的更新

自动合流(Joining), 可中断(Cancellable) 的线程

C++20 同步(Synchronization)库

std::atomic_ref

指定初始化(Designated Initializers)

航天飞机操作符 <=>

范围 for 循环语句支持初始化语句

非类型模板形参支持字符串

[[likely]], [[unlikely]]

日历(Calendar)和时区(Timezone)功能

std::span

特性测试宏

consteval 函数

constinit

用 using 引用 enum 类型

格式化库(std::format)

增加数学常量

std::source_location

[[nodiscard(reason)]]

位运算


🍳参考文献

  1. https://zhuanlan.zhihu.com/p/139515439
  2. https://blog.csdn.net/qq_45254369/article/details/125491031
  3. https://zhuanlan.zhihu.com/p/165389083

🧊文章总结

提示:该文章还在更新中:

  不要走开!作者还在持续更新中,建议点赞关注加收藏!






更多好文推荐

🍸2021-4月Python 机器学习——中文新闻文本标题分类
🍹2021年4月-(计算机网络)小型校园网络模拟搭建,最全最准确版
🍺2022-10-31-基于用户的协同过滤推荐算法实现+MAE+RMSE
🍻2022-11-28-大数据可视化,特征维度大于50
🥂2023-3-9-一篇简短的文章把C++左右值关系讲的透透彻彻

上一篇
End
下一篇
  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

汤姆z

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值