zoukankan      html  css  js  c++  java
  • C++20新特性

    C++20新特性

    新增关键字(keywords)

    concept
    requires
    constinit
    consteval
    co_await
    co_return
    co_yield
    char8_t

    模块(Modules)

    优点:
    1)没有头文件;
    2)声明实现仍然可分离, 但非必要;
    3)可以显式指定导出哪些类或函数;
    4)不需要头文件重复引入宏 (include guards);
    5)模块之间名称可以相同,并且不会冲突;
    6)模块只处理一次, 编译更快 (头文件每次引入都需要处理);
    7)预处理宏只在模块内有效;
    8)模块的引入与引入顺序无关。

    例子:
    Module code (test.ixx)

    // export module xxx.yyy.zzz
    export module cpp20.test;
    
    // Failed to import lib
    //import std.core;
    //import std.filesystem;
    
    /////////////////////////////////////////////
    // common export
    export auto GetCommonString() {
        return "Welcome to learn C++20!";
    }
    
    auto GetSpecificString() {
        return "Welcome to learn C++20!";
    }
    
    // error.Because std::core has been not imported
    //export std::string GetString() {
    //    return "GetString.";
    //}
    
    /////////////////////////////////////////////
    
    /////////////////////////////////////////////
    // entirety export
    export namespace test {
        auto GetTestString() {
            return "Test Test Test!!!";
        }
    
        template <typename T>
        T Sum(T t)
        {
            return t;
        }
        template <typename T, typename ...Args>
        T Sum(T one, Args ...args)
        {
            return one + Sum<T>(args...);
        }
    
    
        enum class ValueType
        {
            kBool=0,
            kChar,
            kInt,
            kFloat,
            kDouble,
        };
        
        template <typename T>
        T GetDataType(ValueType type) {
            switch (type)
            {
            using enum ValueType;
            case kBool:
                return true;
            case kChar:
                return 'A';
            case kInt:
                return 5;
            case kFloat:
                return 12.257902012398877;
            case kDouble:
                return 12.257902012398877;
            }
            return true;
        }
    
    }// namespace test
    /////////////////////////////////////////////
    
    /////////////////////////////////////////////
    // struct export
    export namespace StructTest {
        struct Grade {
            int val = 0;
            int level = 5;
        };
    
        void AddVal(Grade& g, int num) {
            g.val += num;
            g.level += num;
        }
    }// namespace StructTest
    /////////////////////////////////////////////
    

    Calling code (main.cpp)

    import cpp20.test;
    
    int main(int argc, char* argv[]) {
        auto ret1 = GetCommonString();
        //auto ret2 = GetSpecificString(); // error
        //auto ret2 = GetString(); // error
    
        using namespace test;
        auto ret3 = GetTestString();
        auto ret4 = Sum<int>(3, 4);
        auto ret5 = Sum<double>(3.14, 4.62, 9.14);
    
        auto ret6 = GetDataType<bool>(ValueType::kBool);
        auto ret7 = GetDataType<int>(ValueType::kInt);
        auto ret8 = GetDataType<char>(ValueType::kChar);
    
    
        StructTest::Grade grade;
        StructTest::AddVal(grade, 10);
        std::cout << grade.val << " | " << grade.level << std::endl;
    
        return 0;
    }
    

    总结
    1)模块的命名可选择格式为:xxx.yyy.zzz
    2)在MSVC编译器中,模块文件是.ixx,而不是.cpp。(.ixx文件后缀是MSVC编译器中默认的模块接口,C++ 模块接口单元)
    3)普通函数的导出必须添加"export";否则,外部无法调用。也可以选择导出命名空间块。详情可参见上述的例子。
    4)标准库core和文件系统filesystem,提供的函数无法被识别。可能是本人导入module配置操作有误吧!
    5)模块当前支持基本数据类型,比如bool、int、float、char等。也支持结构与类的使用。
    6)模块的定义没有头文件、不会重复编译输出、导入模块没有次序区别,因此建议模块的编写添加相应的命名空间,降低相同符号的可能性。

    详情例子请参见:

    C++20 模块
    VS2019中关于module的配置
    泛化之美--C++11可变模版参数的妙用

    协程(Coroutines)

    进程:操作系统资源分配的基本单元。调度涉及到用户空间和内核空间的切换,资源消耗较大。
    线程:操作系统运行的基本单元。在同一个进程资源的框架下,实现抢占式多任务,相对进程,降低了执行单元切换的资源消耗。
    协程:和线程非常类似。但是转变一个思路实现协作式多任务,由用户来实现协作式调度,即主动交出控制权(或称为用户态的线程)。

    到底什么是协程?

    简单来说,协程就是一种特殊的函数,它可以在函数执行到某个地方的时候暂停执行,返回给调用者或恢复者(可以有一个返回值),并允许随后从暂停的地方恢复继续执行。
    请注意,这个暂停执行不是指将函数所在的线程暂停执行,而是单纯的暂停执行函数本身。
    说白了,用处就是将“异步”风格的编程“同步”化。

    C++20 协程的特点

    1)不需要内部栈分配,仅需要一个调用栈的顶层桢。
    2)协程运行过程中,需要使用关键词来控制运行过程(比如co_return)。
    3)协程可能分配不同线程,触发资源竞争。
    4)没有调度器,但是需要标准和编译器的支持。

    协程的特点在于是一个线程执行,那和多线程相比,协程有何优势?
    优点:
    1)极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销。和多线程比,线程数量越多,协程的性能优势就越明显。
    2)不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

    缺点:
    1)无法利用多核资源:协程的本质是个单线程,它不能同时将单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上。当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
    2)进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序。

    协程中的关键字

    co_wait: 挂起协程, 等待其它计算完成。
    co_return: 从协程返回 (协程 return 禁止使用)。
    co_yield: 返回一个结果并且挂起当前的协程, 下一次调用继续协程的运行。
    注意:上述的协程关键字只能在协程中使用。这也就意味着,在main函数中直接调用co_await xxxx(); 是不行的。

    如何定义与使用协程?

    先了解几个基本的概念:
    1)一个线程只能有一个协程;
    2)协程函数需要返回值是Promise;
    3)协程的所有关键字必须在协程函数中使用;
    4)在协程函数中可以按照同步的方式去调用异步函数,只需要将异步函数包装在Awaitable类中,使用co_wait关键字调用即可。

    理解了以上概念后,就可以按照特定的规则创建和使用协程:
    1)在一个线程中同一个时间只调用一个协程函数,即只有一个协程函数执行完毕了,再去调用另一个协程函数;
    2)使用Awatiable类包装所有的异步函数,一个异步函数处理一请求中的一部分工作(比如执行一次SQL查询,或者执行一次http请求等);
    3)在对应的协程函数中按照需要,通过增加co_wait关键字同步的调用这些异步函数。注意一个异步函数(包装好的Awaiable类)可以在多个协程函数中调用,协程函数可能在多个线程中被调用(虽然一个线程同一时间只调用一个协程函数),所以最好保证Awaiable类是线程安全的,避免出现需要加锁的情况;
    4)在线程中通过调用不同的协程函数响应不同的请求。

    协程一般需要定义三个东西:协程体(coroutine),协程承诺特征类型(Traits),await对象(await)。
    C++20 协程模板:(仅供参考,非官方标准)

    #include <thread>
    #include <coroutine>
    #include <functional>
    #include <windows.h>
    
    // 给协程体使用的承诺特征类型
    struct  CoroutineTraits {        // 名称自定义 |Test|
        struct promise_type {        //名称必须为 |promise_type|
            // 必须实现此接口。(协程体被创建时被调用)
            auto get_return_object() {
                return CoroutineTraits{};
            };
    
            // 必须实现此接口, 返回值必须为awaitable类型。(get_return_object之后被调用)
            auto initial_suspend() {
                return std::suspend_never{};   // never表示继续运行
                //return std::suspend_always{}; // always表示协程挂起
            }
    
            // 必须实现此接口, 返回值必须为awaitable类型。(return_void之后被调用)
            // MSVC需要声明为noexcept,否则报错
            auto final_suspend() noexcept {
                return std::suspend_never{};
            }
    
            // 必须实现此接口, 用于处理协程函数内部抛出错误
            void unhandled_exception() {
                std::terminate();
            }
    
            // 如果协程函数内部无关键字co_return则必须实现此接口。(协程体执行完之后被调用)
            void return_void() {}
    
            // 注意:|return_void|和|return_value| 是互斥的。
            // 如果协程函数内部有关键字co_return则必须实现此接口。(协程体执行完之后被调用)
            //void return_value() {}
    
            // 如果协程函数内部有关键字co_yield则必须实现此接口, 返回值必须为awaitable类型
            auto yield_value(int value) {
                // _valu=value;     // 可对|value|做一些保存或其他处理
                return std::suspend_always{};
            }
        };
    };
    
    // 协程使用的await对象
    struct CoroutineAwaitObj {  // 名称自定义 |CoroutineAwaitObj|
        // await是否已经计算完成,如果返回true,则co_await将直接在当前线程返回
        bool await_ready() const {
            return false;
        }
    
        // await对象计算完成之后返回结果
        std::string await_resume() const {
            return _result;
        }
    
        // await对象真正调异步执行的地方,异步完成之后通过handle.resume()来是await返回
        void await_suspend(const std::coroutine_handle<> handle) {
            std::jthread([handle, this]() {
                // 其他操作处理
                _result = "Test";
    
                // 恢复协程
                handle.resume();
                         }).detach();
        }
    
        // 将返回值存在这里
        std::string _result;
    };
    
    // 协程体
    // |CoroutineTraits| 并不是返回值,而是协程的特征类型;不可以是void、string等返回类型
    CoroutineTraits CoroutineFunc() {
        std::cout << "Start CoroutineFunc" << std::endl;
    
        auto ret = co_await CoroutineAwaitObj();
        std::cout << "Return:" << ret << std::endl;
    
        std::cout << "Finish CoroutineFunc" << std::endl;
    }
    
    int main(int argc, char* argv[]) {
        CoroutineFunc();
    
        Sleep(10*1000);
    
        return 0;
    }
    

    协程的执行流程

    5nm8iT.png

    解析:
    1)先执行结构体promise_type() ,创建一个promise对象;
    2)通过promise对象, 执行get_return_object(), 产生一个coroutine_name对象, 并记录handle;
    3)执行initial_suspend(), 根据返回值的await_ready()返回值 判断是否立即执行协程函数, 当返回值中await_ready()返回值为ture则立即执行协程函数, 否则调用返回值的await_suspend挂起协程、跳出到主函数。
    我们这里返回值是std::suspend_always,它的await_ready()始终返回false。
    4)在主函数中调用协程函数CoroutineFunc(),同时将执行权传递给协程函数;
    5)执行协程函数中操作,直到执行co_waitco_returnco_yield
    6)执行awaitable类中的函数await_ready(),根据返回值判断是否将执行权传递给主函数。如果返回值的await_ready返回false,则调用await_suspend();若await_ready返回true,直接执行await_resume(),并返回到主函数(即await_suspend()不被执行);
    7)协程函数已经执行完语句,所以准备返回,这里没有co_return,所以调用的是return_void()函数。如果有co_return,则调用的是return_value()函数;
    8)然后调用final_suspend,协程进行收尾动作。根据final_suspend的返回值的await_ready判断是否立即析构promise对象,返回true则立即析构,否则不立即析构、将执行权交给主函数;请注意:如果是立即析构promise对象,则后续主函数无法通过promise获得相应的值。
    9)返回主函数执行其他操作,return 0。

    协程的储存空间与生命周期

    1)C++20 的设计是无栈协程, 所有的局部状态都储存在堆上。参见:有栈协程与无栈协程
    2)储存协程的状态需要分配空间,分配 frame 的时候,首先会搜索 promise_type 是否有提供 operator new, 然后再搜索全局范围;
    3)也会存在储存分配失败的情况。比如:如果写了 get_return_object_on_allocation_failure() 函数, 那就是失败后的办法, 代替 get_return_object() 来完成工作(可添加关键字 noexcept);
    4)协程结束以后的释放空间也会优先在 promise_type 里面搜索 operator delete, 其次再搜索全局范围.
    5)协程的储存空间只有在运行完 final_suspend 之后才会析构, 或者人为显式地调用 handle.destroy(). 否则协程的存储空间就永远不会释放;如果在 final_suspend 那里停下了, 那么就得在包装函数里面手动调用 handle.destroy(), 否则就会出现内存泄漏。
    6)如果已经运行完毕了 final_suspend, 或者已经被 handle.destroy() 给析构了, 那么协程的储存空间已经被释放了;如果再次对 handle 做任何的操作都会导致段错误。

    协程用例请参考:

    C++20 协程(coroutine)
    C++20 Coroutines 协程
    深入浅出c++协程

    Ranges & Views

    范围(ranges):是“项目集合”或“可迭代事物”的抽象。最基本的定义只需要存在begin()和end()在范围内。Range 代表一串元素, 或者一串元素中的一段类似 begin/end 对。
    视图(view):其意义可以参考string_view,它的拷贝代价是很低的,需要拷贝的时候直接传值即可,不必传引用。
    ranges通过增加了一种叫做view(视图)的概念,实现了Lazy Evaluation(惰性求值),并且可以将各种view的关系转化用符号“|”串联起来。
    范围适配器(range adaptor):可以将一个range转换为一个view(也可以将一个view转换为另一个view)。范围适配器接受 viewable_range 为其第一参数并返回一个 view 。

    常见的范围适配器:

    适配器 描述
    views::filter 由 range 中满足某个谓词的元素构成的 view
    views::transform 对序列的每个元素应用某个变换函数的 view
    views::take 由另一 view 的前 N 个元素组成的 view
    views::join 由拉平 range 的 view 所获得的序列构成的 view
    views::elements 选取仿 tuple 值组成的 view 和数值 N ,产生每个 tuple 的第 N 个元素的 view
    views::drop 由另一 view 跳过首 N 个元素组成的 view
    views::all 包含 range 的所有元素的 view
    views::take_while 由另一 view 的到首个谓词返回 false 为止的起始元素组成的 view
    views::drop_while 由另一 view 跳过元素的起始序列,直至首个谓词返回 false 的元素组成的 view
    views::split 用某个分隔符切割另一 view 所获得的子范围的 view
    views::common 转换 view 为 common_range
    views::reverse 以逆序迭代另一双向视图上的元素的 view
    views::istream_view 由在关联的输入流上相继应用 operator>> 获得的元素组成的 view
    views::keys 选取仿 pair 值组成的 view 并产生每个 pair 的第一元素的 view
    views::values 选取仿 pair 值组成的 view 并产生每个 pair 的第二元素的 view

    Ranges采用了C++ 20的最新特性Concepts,并且是惰性执行的。
    下面是常见的range类型:

    概念 描述
    std::ranges::input_range 可以从头到尾重复至少一次(只可单次遍历的单向range)
    比如 std::forward_list, std::list, std::deque, std::array
    std::ranges::forward_range 可以从头到尾重复多次(可以多次遍历的单向range)
    比如 std::forward_list, std::list, std::deque, std::array
    std::ranges::bidirectional_range 迭代器还可以向后移动(双向range)
    比如 std::list, std::deque, std::array
    std::ranges::random_access_range 可以恒定时间跳转到元素(支持随机访问的range)
    比如 std::deque, std::array
    std::ranges::contiguous_range 元素总是连续存储在内存中(内容上连续的range)
    比如 std::array

    范围适配器的简单实践

    void Test() {
        using namespace std::ranges;
    
        std::string content{ "Hello! Welcome to learn new feature of C++ 20" };
        for (auto word : content | views::split(' ')) {
            std::cout << "-> ";
            for (char iter : word |
                 views::transform([](char val) { return std::toupper(val); }))
                std::cout << iter;
        }
        std::cout << std::endl;
        // -> HELLO!-> WELCOME-> TO-> LEARN-> NEW-> FEATURE-> OF-> C++-> 20
    
        std::vector<int> vet{ 0, 45, 15, 100, 0, 0, 11, 48, 0, 3, 99, 4, 0, 0, 0, 1485 , 418, 116, 0 };
        std::vector<int> pat{ 0, 0 };
        for (auto part : vet | views::split(pat)) {
            std::cout << "-> ";
            for (int iter : part)
                std::cout << iter << ' ';
        }
        std::cout << std::endl;
        // -> 0 45 15 100 -> 11 48 0 3 99 4 -> 0 1485 418 116 0
    
        std::vector<std::string> data{ "Hello!", " Welcome to", " Learn"," C++ 20" };
        for (char iter : data
             | views::join     // 注意,join不需要添加()
             | views::transform([](char val) { return std::tolower(val); })) {
            std::cout << iter;
        }
        std::cout << std::endl;
        // hello! welcome to learn c++ 20
    }
    
    void Test1() {
        using namespace std::ranges;
        std::string str{ "Hello! Welcome to learn new feature of C++ 20" };
    
        // 自定义范围适配器
        auto newAdaptor =
            views::transform([](char val) { return std::toupper(val); })
            | views::filter([](char val) { return !std::isspace(val); });
    
        for (char iter : str | newAdaptor) {
            std::cout << iter;
        }
        // HELLO!WELCOMETOLEARNNEWFEATUREOFC++20
    }
    
    int main(int argc, char* argv[]) {
        Test();
        std::cout << std::endl;
        Test1();
        return 0;
    }
    

    Ranges对容器的排序

    void Test1() {
        std::vector<int> vec{ 15,18,50,2,99,14,8,33,84,78 };
    
        // before C++20
        //std::sort(vec.begin(), vec.end(), std::greater());
        //std::sort(vec.begin() + 2, vec.end(), std::greater());
    
        // C++20
        // 全排序
        std::ranges::sort(vec, std::ranges::less());
        // 仅对第二个元素之后的所有元素进行排序
        //std::ranges::sort(std::views::drop(vec, 2), std::ranges::greater());
        // 反向排序
        std::ranges::sort(std::views::reverse(vec))
    
        for (auto& iter : vec) {
            std::cout << iter << " ";
        }
    }
    
    struct  Data {
        std::string name;
        std::string addr;
        // 升序排序
        bool operator <(const Data& other)const {
            return name < other.name;
        }
        // 降序排序
        bool operator >(const Data& other)const {
            return name > other.name;
        };
    };
    
    void Test2() {
        std::vector<Data> strVet;
        strVet.emplace_back(Data{ "Jason","Jason house" });
        strVet.emplace_back(Data{ "Lily","Lily house" });
        strVet.emplace_back(Data{ "Mark","Mark house" });
    
        std::ranges::sort(strVet, std::less<Data>());
    
        for (auto& iter : strVet) {
            std::cout << iter.name << " ";
        }
    }
    
    int main(int argc, char* argv[]) {
        Test1();
        Test2();
        return 0;
    }
    

    Ranges & Views 框架的实践

    题目:对1-100求平方和,筛选出前5个能被4整除的数值。
    思路步骤:
    1)将1-100存放在std::vector容器中;
    2)对容器中的每个数值求平方和;
    3)筛选出所有能被4整除的数值;
    4)输出前5个。

    实现:

    // before C++20
    void Test1() {
        // 筛选N个
        constexpr unsigned num = 5;
        std::vector<int> vet(100);
        std::vector<int> newVet;
        // 升序初始化
        std::iota(vet.begin(), vet.end(), 1);
    
        std::transform(vet.begin(), vet.end(), vet.begin(),
                       [](int val) { return val * val; }
        );
        std::copy_if(vet.begin(), vet.end(), std::back_inserter(newVet),
                     [](int val) { return val % 4 == 0; }
        );
    
        for (unsigned i = 0; i < num; i++) {
            std::cout << newVet[i] << ' ';
        }
        // 4 16 36 64 100
    }
    
    // C++20 普通版
    void Test2() {
        constexpr unsigned num = 5;
        std::vector<int> vec(100);
        std::iota(vec.begin(), vec.end(), 1);
    
        auto even = [](const int& a) {
            return a % 4 == 0;
        };
    
        auto square = [](const int& a) {return a * a; };
    
        for (auto iter : std::views::take(std::views::filter(std::views::transform(vec, square), even), num)) {
            std::cout << iter << ' ';
        }
        // 4 16 36 64 100
    }
    
    // C++20 进阶版
    void Test3() {
        using namespace std::ranges;
        constexpr unsigned num = 5;
    
        for (auto iter : views::iota(1)
             | views::transform([](int val) { return val * val; })
             | views::filter([](int val) { return val % 4 == 0; })
             | views::take(num)) {
            std::cout << iter << ' ';
        }
        // 4 16 36 64 100
    }
    
    int main(int argc, char* argv[]) {
        Test1();
        std::cout << std::endl;
        Test2();
        std::cout << std::endl;
        Test3();
        return 0;
    }
    

    Lambda 表达式的更新

    1)允许[=, this]作为Lambda捕获,并弃用此隐式捕获[=];
    2)Lambda init-capture 中的包扩展:...args = std::move(args)](){};
    3)static, thread_local, 和 Lambda 捕获结构化绑定;
    4)模板形式 Lambda。

    模板形式的 Lambda 表达式

    // Before C++20 获取 vector 元素类型
    auto func = [](auto vec){ 
        using T = typename decltype(vec)::value_type; 
    }
    
    // C++20 
    auto func = []<typename T>(vector<T> vec){ 
        // ... 
    }
    

    Lambda 表达式捕获支持打包展开

    // Before C++20
    template<class F, class... Args> 
    auto delay_invoke(F f, Args... args){ 
        return [f, args...]{ 
            return std::invoke(f, args...); 
        } 
    }
    
    // C++20
    template<class F, class... Args> 
    auto delay_invoke(F f, Args... args){ 
        // Pack Expansion:  args = std::move(args)...  
        return [f = std::move(f), args = std::move(args)...](){ 
            return std::invoke(f, args...); 
        } 
    }
    

    原子(Atomic)智能指针

    智能指针(shared_ptr)线程安全吗?
    是: 引用计数控制单元线程安全, 保证对象只被释放一次
    否: 对于数据的读写没有线程安全

    如何将智能指针变成线程安全?
    1)使用 mutex 控制智能指针的访问
    2)使用全局非成员原子操作函数访问, 诸如: std::atomic_load(), atomic_store(), …
    缺点: 容易出错, 开发过程中容易遗漏添加这些操作。

    C++20提供了原子智能指针,比如:atomic<shared_ptr>, atomic<weak_ptr>
    内部原理可能使用了mutex;
    全局非成员原子操作函数标记为不推荐使用(deprecated)

    详情请参见:

    shared_ptr的线程安全性
    std::atomic(std::shared_ptr)

    例子:

    template<typename T> 
    class concurrent_stack { 
        struct Node { 
            T t; 
            shared_ptr<Node> next; 
        }; 
        atomic_shared_ptr<Node> head; 
        // C++11: 去掉 "atomic_" 并且在访问时, 需要用 
        // 特殊的函数控制线程安全, 例如用std::tomic_load 
    public: 
        class reference { 
            shared_ptr<Node> p; 
            <snip> 
        }; 
        auto find(T t) const { 
            auto p = head.load(); // C++11: atomic_load(&head) 
            while (p && p->t != t) 
                p = p->next; 
            return reference(move(p)); 
        } 
        auto front() const { 
            return reference(head); 
        } 
        void push_front(T t) { 
            auto p = make_shared<Node>(); 
            p->t = t; p->next = head; 
            while (!head.compare_exchange_weak(p->next, p)){ 
        } // C++11: atomic_compare_exchange_weak(&head, &p->next, p); }     
        void pop_front() { 
            auto p = head.load(); 
            while (p && !head.compare_exchange_weak(p, p->next)) {
            } // C++11: atomic_compare_exchange_weak(&head, &p, p->next); 
        } 
    };
    

    上述例子来自 Herb Sutter 的 N4162 论文

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

    std::jthread对象包含std::thread一个成员,提供完全相同的公共函数,这些函数只是向下传递调用。这使我们可以将任何内容更改std::thread为std::jthread,确保它将像以前一样工作。

    自动合流(Joining)

    C++20 在线程thread中新增了std::jthread
    功能:
    1)支持中断;
    2)析构函数中自动调用 join();
    3)析构函数调用 stop_source.request_stop() 然后 join()。

    例子:

    // Before C++20
    void Test() {
        std::thread  th;
        {
            th = std::thread([]() {
                for (unsigned i = 1; i < 10; ++i) {
                    std::cout << i << " ";
                    Sleep(500);
                }
                             });
        }
    
        // 如果没有join(),直接退出就会引发崩溃
        // th.join();
    }
    
    // C++20
    void Test1() {
        std::jthread  th;
        {
            th = std::jthread([]() {
                for (unsigned i = 1; i < 10; ++i) {
                    std::cout << i << " ";
                    Sleep(500);
                }
                              });
        }
    
        // 没有使用join也不会崩溃,因为std::jthread的析构函数默认调用join()
    }
    
    int main(int argc, char* argv[]) {
        //Test();
        std::cout << std::endl;
        Test1();
        return 0;
    }
    
    

    可协作的中断(Cancellable)

    在上述例子中使用的[ for (unsigned i = 1; i < 10; ++i) ],循环是10次;如果替换为while(1)时,整个函数就会被阻塞,阻塞在join()。因此线程没有执行结束并正常退出,此时函数join()就会一直等待下去。
    在C++20中提供了可协作的中断操作,可以通过外部发起的请求,最后由线程内部决定是否中断并退出。

    语法说明

    std::stop_token
    用来查询线程是否中断,可以和condition_variable_any配合使用

    std::stop_source
    用来请求线程停止运行,stop_resources 和 stop_tokens 都可以查询到停止请求

    std::stop_callback
    如果对应的stop_token 被要求终止, 将会触发回调函数。
    用法: std::stop_callback StopTokenCallback(OnStopToken, []{ /* … */ });

    例子:

    void Test3() {
        std::jthread  th;
        {
            th = std::jthread([]() {
                while (1) {
                    std::cout << "1";
                    Sleep(500);
                }
                              });
        }
    
        // 外部发起中断请求,但是线程内部没有响应,仍然会阻塞
        th.request_stop();
    
        // 此句执行了,但是整个函数退出时仍会阻塞
        std::cout << "Finish Test3.";
    }
    
    void Test4() {
        std::jthread  th;
        {
            th = std::jthread([](const std::stop_token st) {
                while (!st.stop_requested()) {
                    // 没有收到中断请求,则执行
                    std::cout << "1";
                    Sleep(500);
                }
                              });
        }
    
        Sleep(10 * 1000);
    
        // 外部发起中断请求
        auto ret = th.request_stop();
    }
    
    int main(int argc, char* argv[]) {
        //Test3();
        //std::cout << std::endl;
    
        Test4();
        std::cout << std::endl;
    
        return 0;
    }
    

    三路比较运算符(<=>)

    C++20之前,封装好的对象(比如类对象或结构体对象)若出现比较或排序的情况,就需要重载某个特定的运算符,有时候还需要多个不同的运算符重载。
    C++20,提供了三路比较运算符,会默认生成一系列的比较运算符。生成的默认运算符有六个即:==、!=、<、>、<=、>=。

    详情说明请参见:

    比较运算符
    默认比较

    例子:
    简单来说,对比于双目运算符(:?),多了一处相等比较的返回。

    双目运算符:
    a >= b ? b : a
    
    三路运算符语法:
    (a <=> b) < 0   // 如果 a < b 则为 true
    (a <=> b) > 0   // 如果 a > b 则为 true
    (a <=> b) == 0  // 如果 a 与 b 相等或者等价,则为 true
    
    三路运算符展开:
    auto res = a <=> b;
    if (res < 0)
        std::cout << "a 小于 b";
    else if (res > 0)
        std::cout << "a 大于 b";
    else
        std::cout << "a 与 b 相等";
    // 类似于C的strcmp 函数返回-1, 0, 1
    

    在C++20之前,在map中以结构信息作为Key,必须提供一个排序的仿函数。如下例子:

    struct UserInfo {
        std::string name;
        std::string addr;
    };
    
    struct Compare {
        bool operator()(const UserInfo& left, const UserInfo& right) const {
            return  left.name > right.name;
        }
    };
    
    int main(int argc, char* argv[]) {
        std::map <UserInfo, bool, Compare> infoMap;
    
        UserInfo usr1{ "Jason","Jason1111" };
        UserInfo usr2{ "Lily","Lily2222" };
        UserInfo usr3{ "Mark","Mark3333" };
    
        infoMap.insert(std::pair<UserInfo, bool>(usr2, true));
        infoMap.insert(std::pair<UserInfo, bool>(usr1, false));
        infoMap.insert(std::pair<UserInfo, bool>(usr3, true));
    
        for (auto& iter : infoMap) {
            std::cout << iter.first.name << std::endl;
        }
        return 0;
    }
    

    在C++20中,可以直接使用默认的三路比较运算符。若某运算符不满足,亦可自定义功能。如下:

    struct UserInfo {
        std::string name;
        std::string addr;
    
        // 默认升序
        //std::strong_ordering operator<=>(const UserInfo&) const = default;
    
        // 自定义不同的排序--降序
        std::strong_ordering operator<=>(const UserInfo& info) const {
            auto ret = name <=> info.name;
            return ret > 0 ? std::strong_ordering::less
                : (ret == 0 ? std::strong_ordering::equal : std::strong_ordering::greater);
        };
    };
    
    int main(int argc, char* argv[]) {
        std::map <UserInfo, bool> infoMap;
    
        UserInfo usr1{ "Jason","Jason1111" };
        UserInfo usr2{ "Lily","Lily2222" };
        UserInfo usr3{ "Mark","Mark3333" };
    
        infoMap.insert(std::pair<UserInfo, bool>(usr2, true));
        infoMap.insert(std::pair<UserInfo, bool>(usr1, false));
        infoMap.insert(std::pair<UserInfo, bool>(usr3, true));
    
        for (auto& iter : infoMap) {
            std::cout << iter.first.name << std::endl;
        }
        return 0;
    }
    

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

    标准标头chrono文档

    Calendar

    简单的日期时间转换

    // creating a year
    auto y1 = year{ 2021 };
    auto y2 = 2021y;
    
    // creating a mouth
    auto m1 = month{ 9 };
    auto m2 = September;
    
    // creating a day
    auto d1 = day{ 24 };
    auto d2 = 24d;
    
    weeks w{ 1 }; // 1 周
    days d{ w };  // 将 1 周转换成天数
    std::cout << d.count();
    
    hours h{ d };  // 将 1 周转换成小时
    std::cout << h.count();
    
    minutes m{ w }; // 将 1 周转换成分钟
    std::cout << m.count();
    

    日期时间的计算

    struct DaysAttr {
        sys_days sd;
        sys_days firstDayOfYear;
        sys_days lastDayOfYear;
        year y;
        month m;
        day d;
        weekday wd;
    };
    
    DaysAttr GetCurrentDaysAttr() {
        // 目的获取今年的第一天和最后一天,统一初始化
    
        DaysAttr attr;
        attr.sd = floor<days>(system_clock::now());
        year_month_day ymd = attr.sd;
        attr.y = ymd.year();
        attr.m = ymd.month();
        attr.d = ymd.day();
        attr.wd = attr.sd;
        attr.firstDayOfYear = attr.y / 1 / 1;
        attr.lastDayOfYear = attr.y / 12 / 31;
    
        return attr;
    }
    
    // 一年中过去的天数,以及一年中剩余的天数
    void OverDaysOfYear() {
        // 这会打印出一年中的天数,其中1月1日为第1天,然后还会打印出该年中剩余的天数(不包括)sd。执行此操作的计算量很小。
        // 将每个结果除以days{1}一种方法可以提取整整类型中的天数dn并将其dl分成整数,以进行格式化。
    
        auto arrt = GetCurrentDaysAttr();
        auto dn = arrt.sd - arrt.firstDayOfYear + days{ 1 };
        auto dl = arrt.lastDayOfYear - arrt.sd;
        std::cout << "It is day number " << dn / days{ 1 } << " of the year, "
            << dl / days{ 1 } << " days left." << std::endl;
    }
    
    // 该工作日数和一年中的工作日总数
    void WorkDaysOfYear() {
        // wd是|attr.wd = attr.sd|计算的星期几(星期一至星期日)。
        // 要执行这个计算,我们首先需要的第一个和最后一个日期wd的当年y。|arrt.y / 1 / arrt.wd[1]|是wd一月的第一个,|arrt.y / 12 / arrt.wd[last]|则是wd十二月的最后一个。
        // wd一年中的总数仅是这两个日期之间的周数(加1)。子表达式[lastWd - firstWd]是两个日期之间的天数。将该结果除以1周将得到一个整数类型,该整数类型保存两个日期之间的周数。
        // 星期数的计算方法与星期总数的计算方法相同,不同的是星期数从当天开始而不是wd一年的最后一天开始|sd - firstWd|。
    
        auto arrt = GetCurrentDaysAttr();
        sys_days firstWd = arrt.y / 1 / arrt.wd[1];
        sys_days lastWd = arrt.y / 12 / arrt.wd[last];
        auto totalWd = (lastWd - firstWd) / weeks{ 1 } + 1;
        auto n_wd = (arrt.sd - firstWd) / weeks{ 1 } + 1;
        std::cout << format("It is {:%A} number ", arrt.wd) << n_wd << " out of "
            << totalWd << format(" in {:%Y}.}", arrt.y) << std::endl;;
    }
    
    // 该工作日数和一个月中的工作日总数
    void WorkDaysAndMonthOfYear() {
        // 从wd年月对的第一个和最后一个开始|arrt.y / arrt.m|,而不是整个全年开始
    
        auto arrt = GetCurrentDaysAttr();
        sys_days firstWd = arrt.y / arrt.m / arrt.wd[1];
        sys_days lastWd = arrt.y / arrt.m / arrt.wd[last];
        auto totalWd = (lastWd - firstWd) / weeks{ 1 } + 1;
        auto numWd = (arrt.sd - firstWd) / weeks{ 1 } + 1;
        std::cout << format("It is {:%A} number }", arrt.wd) << numWd << " out of "
            << totalWd << format(" in {:%B %Y}.", arrt.y / arrt.m) << std::endl;;
    }
    
    // 一年中的天数
    void DaysOfYear() {
        auto arrt = GetCurrentDaysAttr();
        auto total_days = arrt.lastDayOfYear - arrt.firstDayOfYear + days{ 1 };
        std::cout << format("Year {:%Y} has ", y) << total_days / days{ 1 } << " days." << std::endl;;
    }
    
    // 一个月中的天数
    void DaysOfMonth() {
        // 表达式|arrt.y / arrt.m / last|是年份-月份对的最后一天,|arrt.y / arrt.m|就是|arrt.y / arrt.m / 1|月份的第一天。
        // 两者都转换为sys_days,因此可以减去它们以得到它们之间的天数。从1开始的计数加1。
    
        auto arrt = GetCurrentDaysAttr();
        auto totalDay = sys_days{ arrt.y / arrt.m / last } - sys_days{ arrt.y / arrt.m / 1 } + days{ 1 };
        std::cout << format("{:%B %Y} has ", arrt.y / arrt.m) << totalDay / days{ 1 } << " days." << std::endl;;
    }
    

    语法初始化

    对于部分不喜欢“常规语法”的开发者,可以使用完整的“构造函数语法”来代替。

    例如:
    sys_days newYear = y/1/1;
    sys_days firstWd = y/1/wd[1];
    sys_days lastWd = y/12/wd[last];
    
    可以替换为:
    sys_days newYear = year_month_day{y, month{1}, day{1}};
    sys_days firstWd = year_month_weekday{y, month{1}, weekday_indexed{wd, 1}};
    sys_days lastWd = year_month_weekday_last{y, month{12}, weekday_last{wd}};
    

    Timezone

    time_zone表示特定地理区域的所有时区转换。
    C++语言标准记得选择:/std:c++latest

    例子:

    int main()
    {
        constexpr std::string_view locations[] = {
            "Africa/Casablanca",   "America/Argentina/Buenos_Aires",
            "America/Barbados",    "America/Indiana/Petersburg",
            "America/Tarasco_Bar", "Antarctica/Casey",
            "Antarctica/Vostok",   "Asia/Magadan",
            "Asia/Manila",         "Asia/Shanghai",
            "Asia/Tokyo",          "Atlantic/Bermuda",
            "Australia/Darwin",    "Europe/Isle_of_Man",
            "Europe/Laputa",       "Indian/Christmas",
            "Indian/Cocos",        "Pacific/Galapagos",
        };
        constexpr auto width = std::ranges::max_element(locations, {},
            [](const auto& s) { return s.length(); })->length();
     
        for (const auto location : locations) {
            try {
                // may throw if `location` is not in the time zone database
                const std::chrono::zoned_time zt{location, std::chrono::system_clock::now()};
                std::cout << std::setw(width) << location << " - Zoned Time: " << zt << '
    ';
            } catch (std::chrono::nonexistent_local_time& ex) {
                std::cout << "Error: " << ex.what() << '
    ';
            }
        }
    }
    

    consteval 与 constinit

    constexpr

    既能参与函数的声明,又能参与变量的声明
    constexpr可用于编译或运行时函数,它的结果是常量。
    constexpr的主要作用是声明变量的值或函数的返回值可以在常量表达式(即编译期便可计算出值的表达式)中使用。

    int Func() {
        return 1;
    }
    
    // 可修改为
    //constexpr int Func() {
    //    return 1;
    //}
    
    constexpr const int x = 5;  // OK
    constexpr const int y = Func(); // Error
    

    consteval

    只能参与函数的声明
    当某个函数使用consteval声明后,则所有带有求值的操作,来调用这个函数的表达式时,必须为常量表达式。
    实际上是编译时运行的函数,也就是它的参数在编译时是“确定的”(常量),它的结果也是常量。

    例子:

    consteval int Test1(int val) {
        return ++val;
    }
    constexpr int Test2(int val) {
        return ++val;
    }
    
    int main(int argc, char* argv[]) {
        int ret = Test1(10);
        std::cout << ret << std::endl;
        //int val = Test1(ret);   //error , ret is not const
        int val = Test2(ret);
        std::cout << val;
        return 0;
    }
    

    如上例子所示:
    ret是函数返回的变量,而由consteval定义的函数必须在编译时可运行出常量结果,因此冲突了。int val = Test1(ret)无法被调用。
    constexpr既可在编译时也是可以在运行时,因此可以接受变量参数。

    constinit

    只能参与变量的声明
    constinit只能用于static或thread_local,不能与constexpr、consteval一起使用。
    constinit的作用在于显式地指定变量的初始化方式为静态初始化。
    其生命周期必须为静态生命周期或线程本地(Thread-local)生命周期(即不能为局部变量),其初始化表达式必须是一个常量表达式。

    constexpr的变量是const类型,只读,不能二次修改;constinit是说变量在程序开始时被初始化,是static类型,不能在运行时被创建,变量不要求是const类型,可以被二次修改。

    例子:

    consteval int Test1() {
        return 1;
    }
    
    int Test2() {
        return 2;
    }
    
    void Test3() {
        constinit int e = 20;  // Error: e is not static
    }
    
    constinit int a = 100;    // OK
    constinit int b = Test1(); // OK,run time
    constinit thread_local int c = 200; // OK
    constinit int d = Test2(); // Error: `Test2()` is not a constant expression
    
    int Test4() {
        // constinit can be modified
        a += 200;   // run time
        b = 2000;
        c -= 50;
        return a;
    }
    

    用 using 引用 enum 类型

    enum class Color {
        kRed,
        kBlue,
        kGreen,
    };
    
    // before C++20
    std::string_view Color2String(const Color color) {
        switch (color) {
        case Color::kRed:
            return "Red";
        case Color::kBlue:
            return "Blue";
        case Color::kGreen:
            return "Green";
        }
        return "Red";
    }
    
    // C++20
    std::string_view Color2String(const Color color) {
        switch (color) {
            using enum Color;  // feature
        case kRed:
            return "Red";
        case kBlue:
            return "Blue";
        case kGreen:
            return "Green";
        }
        return "Red";
    }
    

    实现枚举量值与枚举量值的映射,推荐一个专门处理enum转化的库----Better Enums

  • 相关阅读:
    Redis 注册为系统服务,修改账号密码
    HDFS源码分析数据块复制监控线程ReplicationMonitor(二)
    HDFS源码分析数据块复制监控线程ReplicationMonitor(一)
    HDFS源码分析之UnderReplicatedBlocks(二)
    HDFS源码分析之UnderReplicatedBlocks(一)
    HDFS源码分析之LightWeightGSet
    HDFS源码分析数据块汇报之损坏数据块检测checkReplicaCorrupt()
    HDFS源码分析之数据块及副本状态BlockUCState、ReplicaState
    HDFS中数据节点数据块存储示例
    HDFS源码分析之数据块Block、副本Replica
  • 原文地址:https://www.cnblogs.com/gd-luojialin/p/15423391.html
Copyright © 2011-2022 走看看