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 << '\n';
        } catch (std::chrono::nonexistent_local_time& ex) {
            std::cout << "Error: " << ex.what() << '\n';
        }
    }
}

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM