C++11『lambda表達式 ‖ 線程庫 ‖ 包裝器』

?個人主頁: 北 海
🎉所屬專欄: C++修行之路
🎃操作環境: Visual Studio 2022 版本 17.6.5

成就一億技術人


文章目錄

  • 🌇前言
  • 🏙?正文
    • 1.lambda表達式
      • 1.1.仿函數的使用
      • 1.2.lambda表達式的語法
      • 1.3.lambda表達式的使用
      • 1.4.lambda表達式的原理
      • 1.4.lambda表達式的優點及適用場景
    • 2.線程庫
      • 2.1.thread 線程類
        • 2.1.1.this_thread 命名空間
      • 2.2.mutex 互斥鎖類
        • 2.2.1.并行與串行的對比
        • 2.2.2.其他鎖類型
        • 2.2.3.RAII 風格的鎖
      • 2.3.condition_variable 條件變量類
        • 2.3.1.交替打印數字
      • 2.4.atomic 原子操作類
    • 3.包裝器
      • 3.1.function 包裝器
      • 3.2.bind 綁定
  • 🌆總結


🌇前言

自從C++98以來,C++11無疑是一個相當成功的版本更新。它引入了許多重要的語言特性和標準庫增強,為C++編程帶來了重大的改進和便利。C++11的發布標志著C++語言的現代化和進步,為程序員提供了更多工具和選項來編寫高效、可維護和現代的代碼


🏙?正文

1.lambda表達式

lambda 表達式 源于數學中的 λ 演算,λ 演算是一種 基于函數的形式化系統,它由數學家 阿隆佐邱奇 提出,用于研究抽象計算和函數定義。對于編程領域來說,可以使用 lambda 表達式 快速構建函數對象,作為函數中的參數

1.1.仿函數的使用

仿函數C++ 中的概念,指借助 類+operator()重載 創建的函數對象,仿函數 的使用場景如下

創建一個 vector,通過 sort 函數進行排序,至于結果為升序還是降序,可以通過 仿函數 控制

#include <iostream>
#include <unordered_map>
#include <iostream>
#include <vector>
#include <algorithm>using namespace std;struct cmpLess
{bool operator()(int n1, int n2){return n1 < n2;}
};struct cmpGreater
{bool operator()(int n1, int n2){return n1 > n2;}
};int main()
{vector<int> arr = { 8,5,6,7,3,1,1,3 };sort(arr.begin(), arr.end(), cmpLess()); // 升序cout << "升序: ";for (auto e : arr)cout << e << " ";cout << endl;sort(arr.begin(), arr.end(), cmpGreater()); // 降序cout << "降序: ";for (auto e : arr)cout << e << " ";cout << endl;return 0;
}

注:sort 如果不傳遞函數對象,默認排序結果為升序

結果為正確排序,但這種先創建一個仿函數對象,再調用的傳統寫法有點麻煩了,如果是直接使用 lambda 表達式 創建函數對象,整體邏輯會清楚很多

使用 lambda 表達式 修改后的代碼如下,最大的改變就是 可以直接在傳參時直接編寫函數對象的代碼邏輯

#include <iostream>
#include <unordered_map>
#include <iostream>
#include <vector>
#include <algorithm>using namespace std;int main()
{vector<int> arr = { 8,5,6,7,3,1,1,3 };sort(arr.begin(), arr.end(), [](int n1, int n2) { return n1 < n2; }); // 升序cout << "升序: ";for (auto e : arr)cout << e << " ";cout << endl;sort(arr.begin(), arr.end(), [](int n1, int n2) { return n1 > n2; }); // 降序cout << "降序: ";for (auto e : arr)cout << e << " ";cout << endl;return 0;
}

最終結果也是正常的

有了 lambda 表達式 之后,程序員不必再通過 仿函數 構建函數對象,并且可以在一定程度上提高代碼的可閱讀性,比如一眼就可以看出回調函數是在干什么

接下來看看如何理解 lambda 表達式 語法

1.2.lambda表達式的語法

lambda 表達式 分為以下幾部分:

  • [ ] 捕捉列表
  • ( ) 參數列表
  • mutable 關鍵字
  • ->returntype 返回值類型
  • { } 函數體

[ ]( ) mutable ->returntype { }

其中,( ) 參數列表、mutable->returntype 都可以省略

  • 省略 ( )參數列表 表示當前是一個無參函數對象
  • 省略 mutable關鍵字 表示保持捕捉列表中參數的常量屬性
  • 省略 ->returntype返回值類型 表示具體的返回值類型由函數體決定,編譯器會自動推導出返回值類型

注意:

  • 捕捉列表 和 函數體 不可省略
  • 如果使用了 mutable關鍵字 或者 ->returntype 返回值,就不能省略 ( )參數列表,即使為空
  • 雖然返回值類型編譯器可以推導,但最好還是注明返回值類型

也就是說,最基本的 lambda表達式 只需書寫 [ ]{ } 即可表示,比如這樣

int main()
{// 最簡單的 lambda表達式[]{};return 0;
}

此時的 lambda表達式 相當于一個 參數為空、返回值為空、函數體為空 的匿名函數對象

void func()
{}

主要區別在于 lambda 表達式 構建出來的是一個 匿名函數對象,而 func 是一個 有名函數對象,可以直接調用

1.3.lambda表達式的使用

lambda 表達式 構建出的是一個 匿名函數對象,匿名函數對象也可以調用,不過需要在創建后立即調用,否則就會因為越出作用域而被銷毀(匿名對象生命周期只有一行

下面通過 lambda 表達式 構建一個簡單的 兩整數相加 函數對象并調用

int main()
{int ret = [](int x, int y)->int { return x + y; }(1, 2);cout << ret << endl;return 0;
}

直接使用 lambda 表達式 構建出的 匿名函數對象 比較抽象,一般都是將此 匿名函數對象 作為參數傳遞(比如 sort),如果需要顯式調用,最好是將創建出來的 匿名函數對象 賦給一個 有名函數對象,調用時邏輯會清晰很多

使用 auto 推導 匿名函數對象 的類型,然后創建 add 函數對象

int main()
{auto add = [](int x, int y)->int { return x + y; };int ret = add(1, 2);cout << ret << endl;return 0;
}

lambda 表達式 還有很多玩法,接下來逐一介紹,順便學習其他組成部分


利用 lambda 表達式 構建一個交換兩個元素的 函數對象

最經典的寫法是 函數參數設為引用類型,傳入兩個元素,在函數體內完成交換

int main()
{int x = 1;int y = 2;cout << "交換前" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;auto swap = [](int& rx, int& ry)->void{auto tmp = rx;rx = ry;ry = tmp;};swap(x, y);cout << "交換后" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;return 0;
}

這種經典寫法毋庸置疑,肯定能完成兩數交換的任務

除此之外,還可以借助 lambda表達式 中的 捕捉列表 捕獲外部變量進行交換

int main()
{int x = 1;int y = 2;cout << "交換前" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;auto swap = [x, y]() ->void{auto tmp = x;x = y;y = tmp;};swap();cout << "交換后" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;return 0;
}

因為現在 函數對象 是直接捕獲外部變量進行操作,調用函數對象時,無需傳參

代碼寫完,編譯器立馬給出了報錯:xy 不可修改

這是因為 捕捉列表 中的參數是一個值類型(傳值捕捉),此時的捕獲的是外部變量的內容,然后賦值到 x、y 中,捕捉列表 中的參數默認具有 常量屬性,不能直接修改,但可以添加 mutable 關鍵字 取消常性

int main()
{int x = 1;int y = 2;cout << "交換前" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;auto swap = [x, y]()mutable ->void{auto tmp = x;x = y;y = tmp;};swap();cout << "交換后" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;return 0;
}

但是程序運行結果不盡人意,外部的 x、y 并沒有被交換,證明此時 捕捉列表 中的參數 x、y 是獨立的值(類似函數中的值傳遞)

想讓外部的 x、y 被真正捕獲,需要使用 引用捕捉

int main()
{int x = 1;int y = 2;cout << "交換前" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;// 引用捕捉auto swap = [&x, &y]() ->void{auto tmp = x;x = y;y = tmp;};swap();cout << "交換后" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;return 0;
}

現在 x、y 被成功交換了

注意: 捕捉列表中的 &x 表示引用捕捉外部的 x 變量,并非取地址(特例)

所以說 mutable 關鍵字不常用,因為它取消的是值類型的常性,即使修改了,對外部也沒有什么意義,如果想修改,直接使用 引用捕捉 就好了


捕捉列表 支持 混合捕捉,同時使用 引用捕捉 + 傳值捕捉

int main()
{int x = 1;int y = 2;cout << "調用前" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;// 混合捕捉auto func = [&x, y]()mutable ->void{x = 100;y = 200;};func();cout << "調用后" << endl;cout << "\tx: " << x << endl << "\ty: " << y << endl;return 0;
}

x 被修改了,而 y 沒有


除了 混合捕捉 外,捕捉列表 還支持 全部引用捕捉全部傳值捕捉

全部引用捕捉

int main()
{int x, y, z, a, b, c;x = y = z = 0;a = b = c = 1;string str = "Hello lambda!";cout << "&str: " << &str << endl << endl;auto func = [&]()->void{cout << x << " " << y << " " << z << " " << endl;cout << a << " " << b << " " << c << " " << endl;cout << str << endl;cout << "&str: " << &str << endl << endl;};func();return 0;
}

無需指定 捕捉列表 中的參數,& 可以一鍵 引用捕捉 外部所有變量

注:只能捕捉已經定義或聲明的變量

全部傳值捕捉

int main()
{int x, y, z, a, b, c;x = y = z = 0;a = b = c = 1;string str = "Hello lambda!";cout << "&str: " << &str << endl << endl;auto func = [=]()->void{cout << x << " " << y << " " << z << " " << endl;cout << a << " " << b << " " << c << " " << endl;cout << str << endl;cout << "&str: " << &str << endl << endl;};func();return 0;
}

全部傳值捕捉 也能一鍵捕捉外部變量,不過此時捕獲的是外部變量的值,并非變量本身,無法對其進行修改(可以通過 mutable關鍵字 取消常性)

注意: [=] 表示全部傳值捕捉,[] 表示不進行捕捉,兩者不等價

捕捉列表 的使用非常靈活,比如 [&, x] 表示 x 使用 傳值捕捉,其他變量使用 引用捕捉[=, &str] 表示 str 使用 引用捕捉,其他變量使用 傳值捕捉

捕捉列表 就像一個 “大師球”,可以直接捕捉到外部的變量,在需要大量使用外部變量的場景中很實用,有效避免了繁瑣的參數傳遞與接收

有沒有 全部引用捕捉 + 全部傳值捕捉

當然沒有,這是相互矛盾的,一個變量不可能同時進行 引用傳遞值傳遞,即便傳遞成功了,編譯器在使用時也不知道使用哪一個,存在二義性,所以不被允許

注意: 關于 捕獲列表 有以下幾點注意事項

  1. 捕捉列表不允許變量重復傳遞,否則就會導致編譯錯誤
  2. 在塊作用域以外的 lambda 函數捕捉列表必須為空
  3. 在塊作用域中的 lambda 函數不僅能捕捉父作用域中局部變量,也能捕捉到爺爺作用域中的局部變量


lambda表達式 還可以完美用作 線程回調函數,比如接下來使用 C++11 中的 thread 線程類,創建一個線程,并使用 lambda 表達式 創建一個線程回調函數對象

int main()
{// 創建線程,并打印線程idthread t([] { cout << "thread running... " << this_thread::get_id() << endl; });t.join();return 0;
}

總之 lambda 表達式 在實際開發中非常好用,關于 thread類的相關知識放到后面講解,接下來先看看 lambda 表達式 的實現原理

1.4.lambda表達式的原理

lambda 表達式 生成的函數對象有多大呢?

是像 普通的函數對象指針 一樣占 4/8 字節,還是像 仿函數 一樣占 1 字節,通過 sizeof 計算大小就可以一探究竟

// 普通函數
int add(int x, int y)
{return x + y;
}// 仿函數
class addFunc
{
public:int operator()(int x, int y){return x + y;}
};int main()
{auto typeA = add;addFunc typeB;auto typeC = [](int x, int y)->int { return x + y; };cout << "普通函數: " << sizeof(typeA) << endl;cout << "仿函數: " << sizeof(typeB) << endl;cout << "lambda表達式: " << sizeof(typeC) << endl;return 0;
}

結果顯示,lambda 表達式 生成的函數對象與 仿函數 生成的函數對象大小是一樣的,都是 1字節


仿函數 生成的函數對象大小為 1字節是因為其生成了一個空類,實際調用時是通過 operator() 重載實現的,比如上面的 addFunc 類,空類因為沒有成員變量,所以大小只為 1字節

由此可以推斷 lambda 表達式 本質上也是生成了一個空類,分別查看使用 仿函數lambda 表達式 時的匯編代碼

可以看到,這兩段匯編代碼的內容是一模一樣的,都是先 call 一個函數(operator() 重載函數),然后再執行主體邏輯(兩數相加),只不過使用 仿函數 需要自己編寫一個 空類,而 使用 lambda 表達式 時由編譯器生成一個 空類,為了避免這個自動生成的 空類 引發沖突,會將這個 空類 命名為 lambda_uuid

uuid通用唯一標識碼,可以生成一個重復率極低的辨識信息,避免類名沖突,這也意味著即便是兩個功能完全一樣的 lambda 表達式,也無法進行賦值,因為 lambda_uuid 肯定不一樣

所以在編譯器看來,lambda 表達式 本質上就是一個 仿函數

1.4.lambda表達式的優點及適用場景

lambda 表達式 作為一種輕量級的匿名函數表示方式,具備以下優點:

  1. 簡潔性: 對于簡單的函數操作,無需再手動創建函數、調用,只需要編寫一個 lambda 表達式生成函數對象
  2. 方便些: lambda 表達式具有 捕捉列表,可以輕松捕獲外部的變量,避免繁瑣的參數傳遞與接收
  3. 函數編程支持: lambda 表達式可以作為函數的參數、返回值或存儲在數據結構中
  4. 內聯定義: lambda 表達式Lambda表達式可以作為函數的參數、返回值或存儲在數據結構中
  5. 簡化代碼: 對于一些簡單的操作,使用 lambda 表達式可以減少代碼的行數,提高代碼的可讀性

總的來說,lambda 表達式 可以替代一些代碼量少的函數,使用起來十分方便,如果 lambda 表達式 編寫出來的代碼過于復雜時,可以考慮轉為普通函數,確保代碼的清晰性和可讀性


2.線程庫

關于 線程 相關操作,Linux 選擇使用的是 POSIX 標準,而 Windows 沒有選擇 POSIX 標準,反而是自己搞了一套 API 和系統調用,稱為 Win32 API,意味著 LinuxWindows 存在標準差異,直接導致能在 Linux 中運行的程序未必能在 Windows 中運行

C++11 之前,編寫多線程相關代碼如果保證兼容性,就需要借助 條件編譯,分別實現兩份代碼,根據不同平臺編譯不同的代碼(非常麻煩)

// 確保平臺兼容性
#ifdef __WIN_32__CreateThread // Windows 中創建線程的接口// ...
#elsepthread_create // Linux 中創建線程的接口// ...
#endif

C++11 中,加入了 線程庫 這個標準,其中包含了 線程、互斥鎖、條件變量 等常用線程操作,并且無需依賴第三方庫,也就意味著使用 線程庫 編寫的代碼既能在 Linux 中運行,也能在 Windows 中運行,保障了代碼的可移植性,除此之外,線程庫 還新加入了 原子相關操作

2.1.thread 線程類

thread 線程類的概況如下

首先看看 thread 類中的 線程 id

Linux 中的 線程 id 表示每個輕量級進程 TCB 的起始地址,用一個 unsigned long int 表示,理解起來比較費勁;在 thread 類中,直接創建了一個 id 類,也就是這里的 thread::id,這個類用于標識 線程,同時在類中重載了一系列 operator 函數,用于兩個 thread::id 對象的比較

線程創建后,系統會為其分配一個類型為 thread::id 的標識符,也就是該線程的唯一標識符

獲取當前線程的 id,并進行比較

int main()
{thread::id id1 = std::this_thread::get_id();thread::id id2 = std::this_thread::get_id();cout << "id1: " << id1 << " " << "id2: " << id2 << endl;if (id1 == id2)cout << "id 相同" << endl;elsecout << "id 不同" << endl;return 0;
}

注意: thread::id 是一個類,不支持初始化或賦值,用于獲取線程 id

至于 thread::native_handle_type 代表一個底層線程的本地(native)句柄或標識符,本地句柄通常是由操作系統提供的,用于標識和管理線程的底層資源

在絕大多數情況下,使用 C++ 標準庫提供的高級線程抽象是足夠的,而無需直接訪問線程的本地句柄。直接使用底層線程句柄通常是為了執行與平臺相關的線程操作,這可能包括與操作系統相關的調度、優先級、特定的線程控制等。這樣的操作通常是為了滿足對底層線程管理的特殊需求,而不是一般性的 C++ 線程編程。

總結就是 thread::native_handle_type 一般用不上,現階段不必關心


接下來看看 構造函數 部分

創建 線程類 對象,支持:

  • 創建一個參數為空的默認線程對象
  • 通過可變參數模板傳入回調函數和參數,其中 Fn 表示回調函數對象,Args 是傳給回調函數的參數包(可以為空)
  • 移動構造,根據線程對象(右值)來構造線程對象

注意: thread 類不支持 拷貝構造,因為線程對象擁有自己的獨立棧等線程資源,所以這里的 拷貝構造 使用 delete 關鍵字刪除了

使用 thread 類需要包含 thread 這個頭文件

#include <iostream>
#include <thread>using namespace std;int main()
{// 參數為空的默認線程對象thread t1; // 傳入回調函數及參數thread t2([](int x, int y)->void { while(true)cout << "x + y = " << x + y << endl; }, 1, 2);// 只傳入回調函數thread t3([]()->void {while(true)cout << "thread running..." << endl; });//t1.join(); // t1 線程狀態為空,不能 join 等待t2.join();t3.join();// 無法拷貝構造//thread t4(t3);return 0;
}

線程回調函數不止可以使用 lambda 表達式,還可以傳入 函數指針 或者 函數對象

通過調試可以看到 t2t3 線程正在運行中,而 t1 因為沒有指定回調函數,所以也就沒有完全創建,自然也就沒有在運行

其中 1739230925964 分別為 主線程、次線程 t2 和 次線程 t3,而 846026080ntdll.dll 類型的線程,用于為應用程序加載其他動態庫,程序運行大概半分鐘后,這兩個線程就會自動消失,因為當前處于調試狀態,并且程序運行時間較短,所以才會看到這個兩個系統級線程

注意: 線程如果沒有完全創建,是不能 join 等待的,并且線程不支持拷貝操作

同樣的,thread 只支持 移動賦值,不支持 傳值賦值

部分構造函數后跟的 noexcept 關鍵字表示當前函數不會拋出 異常,詳細知識放到 『異常』 文章中講解

當線程對象生命周期結束時,會調用 析構函數 銷毀對象


thread 類還提供了一批線程相關接口,比如 獲取 id、等待、分離、交換

除了 joinableswap,其他功能在 pthread 庫中都已經使用過了

  • get_id 對應 pthread_self
  • join 對應 pthread_join
  • detach 對應 pthread_detach

簡單使用如下

int main()
{// 創建線程thread t([]()->void { cout << "thread running..." << endl; });// 獲取線程 idthread::id id = t.get_id();// 線程剝離// t.detach();cout << "線程 " << id << " 已經創建了" << endl;// 等待線程退出t.join();return 0;
}

注意: 分離線程后,主線程運行結束,整個程序也會隨著終止,會導致正在運行中的次線程終止

joinable 是非阻塞版的線程等待函數,等待成功返回 true,否則返回 false

swap 則是將兩個線程的資源進行交換(線程回調函數、線程狀態等)

注意: swap 并不會交換 thread::id,因為這是線程唯一標識符

至于最后兩個函數不常用,這里就不介紹了

這些都是線程常見操作,有了 Linux 多線程編程的基礎,學習起來會輕松很多,接下來編寫一個成員:創建一批線程,并分別打印十次自己的 id

int main()
{vector<thread> vts(5); // 5 個次線程(未完全創建)for (int i = 0; i < 5; i++){// 移動構造vts[i] = thread([]()->void{for (int i = 0; i < 10; i++){// 如何獲取 id ?cout << "我是線程 " << " 我正在運行..." << endl;}});}// 等待線程退出for (auto& t : vts)t.join();return 0;
}

此時面臨一個尷尬的問題:如何在回調函數中獲取線程 id

  • 線程 id 目前之前通過線程對象調用 get_id 函數獲取
  • 傳入線程嗎?不行,因為此時線程還沒有完全創建,線程 id0
  • 傳入線程對象?不行,線程還沒有完全創建,傳入的對象也無法使用,也能通過捕獲列表進行引用捕捉,不過同樣無法使用

如此一來,想要在 線程回調函數 內獲取 線程 id 還不是一件容易的事,好在 C++11 中還提供了一個 this_thread 命名空間,其中提供了獲取 線程 id 等函數,可以自由調用

2.1.1.this_thread 命名空間

this_thread 是一個命名空間,其中包含了 獲取線程 id、線程休眠、線程時間片 相關函數

有了 this_thread 命名空間之后,就可以輕松獲取 線程 id

int main()
{vector<thread> vts(5); // 5 個次線程(未完全創建)for (int i = 0; i < 5; i++){// 移動構造vts[i] = thread([]()->void{for (int i = 0; i < 10; i++){// 獲取 idauto id = this_thread::get_id();cout << "我是線程 " << id << " 我正在運行..." << endl;}});}// 等待線程退出for (auto& t : vts)t.join();return 0;
}

可以看到,正常獲取到了每個線程的 線程 id

注:這里打印錯亂很正常,因為顯示器也是臨界資源,多線程并發訪問時,也是需要加鎖保護的

this_thread 只是一個命名空間,是如何做到正確調用 get_id 函數并獲取線程 id 的?
this_threadstd 中的一個子命名空間,其中包含了一些與線程有關的操作,比如 get_id,當線程調用 this_thread::get_id 時,實際調用的就是該線程的 thread::get_id,所以才能做到誰調用,就獲取誰的線程 id


除此之外,this_thread 命名空間中還提供了 線程休眠 的接口:sleep_untilsleep_for

sleep_util 表示休眠一個 絕對時間,比如線程運行后,休眠至明天 6::00 才接著運行;sleep_for 則是讓線程休眠一個 相對時間,比如休眠 3 秒后繼續運行,休眠 絕對時間 用的比較少,這里來看看如何休眠 相對時間

相對時間 有很多種:時、分、秒、毫秒、微秒…,這些單位包含于 chrono 類中

比如分別讓上面程序中的線程每隔 200 毫秒休眠一次,修改代碼如下

int main()
{vector<thread> vts(5); // 5 個次線程(未完全創建)for (int i = 0; i < 5; i++){// 移動構造vts[i] = thread([]()->void{for (int i = 0; i < 10; i++){// 獲取 idauto id = this_thread::get_id();cout << "我是線程 " << id << " 我正在運行..." << endl;// 休眠 200 毫秒this_thread::sleep_for(chrono::milliseconds(200));}});}// 等待線程退出for (auto& t : vts)t.join();return 0;
}

也可以讓線程休眠其他單位時間


最后在 this_thread 命名空間中還存在一個特殊的函數:yield

這里的 yield 表示 讓步、放棄,帶入多線程環境中就表示 主動讓出當前的時間片

yield 主要用于 無鎖編程(盡量減少使用鎖),而無鎖編程的實現基于 原子操作 CAS,關于原子的詳細知識放到后面講解

原子操作 CAS 是一個不斷重復嘗試的過程,如果嘗試的時間過久,就會影響整體效率,因為此時是在做無用功,而 yield 可以主動讓出當前線程的時間片,避免大量重復,把 CPU 資源讓出去,從而提高整體效率

2.2.mutex 互斥鎖類

多線程編程需要確保 線程安全 問題

首先要明白 線程擁有自己獨立的棧結構,但對于全局變量等 臨界資源,是直接被多個線程共享的

如果想給線程回調函數傳遞 左值引用 類型的參數,需要使用 ref 引用包裝器函數進行包裝傳遞

比如通過以下代碼證明 線程獨立棧 的存在

int g_val = 0;void Func(int n)
{cout << "&g_val: " << &g_val << " &n: " << &n << endl << endl;
}int main()
{int n = 10;thread t1(Func, n);thread t2(Func, n);t1.join();t2.join();return 0;
}

可以看到,全局變量 g_val 的地址是一樣,而局部變量 n 的地址相差很遠,證明這兩個局部變量不處于同一個棧區中,而是分別存在線程的 獨立棧

如果多個線程同時對同一個 臨界資源 進行操作

  • 操作次數較少時,近似原子
  • 操作次數多時,有線程安全問題

這里同時對 g_val 進行 n++ 操作

n = 100 時,結果還算正常(正確結果為 200

int g_val = 0;void Func(int n)
{while (n--)g_val++;
}int main()
{int n = 100;thread t1(Func, n);thread t2(Func, n);t1.join();t2.join();cout << "g_val: " << g_val << endl;return 0;
}

但如果將 n 改為 20000,程序就出問題了(正確結果為 40000

n = 20000;

并且幾乎每一次運行結果都不一樣,這就是由于 線程安全 問題帶來的 不確定性 導致的

關于線程安全的更多知識詳見 Linux多線程【線程互斥與同步】


確保 線程安全 的手段之一就是 加鎖 保護,C++11 中就有一個 mutex 類,其中包含了 互斥鎖 的各種常用操作

比如創建一個 mutex 互斥鎖 對象,當然 互斥鎖也是不支持拷貝的mutex 互斥鎖 類也沒有提供移動語義相關的構造函數,因為鎖資源一般是不允許被剝奪的


互斥鎖 對象的構造很簡單,使用也很簡單,常用的操作有:加鎖、嘗試加鎖、解鎖

  • lock 對應 pthread_mutex_lock
  • try_lock 對應 pthread_mutex_trylock
  • unlock 對應 pthread_mutex_unlock

這些操作使用起來十分簡單,對上面的程序進行加鎖保護

注:使用 mutex 類需要包含 mutex 這個頭文件

int g_val = 0;// 互斥鎖對象
mutex mtx;void Func(int n)
{while (n--){mtx.lock();g_val++;mtx.unlock();}
}int main()
{int n = 20000;thread t1(Func, n);thread t2(Func, n);t1.join();t2.join();cout << "g_val: " << g_val << endl;return 0;
}

此時無論數據量有多大,最終的結果都是符合預期的

注意: 這里的兩個線程只需要一把鎖,并且要保證兩個線程看到的是同一把鎖

2.2.1.并行與串行的對比

互斥鎖 的加鎖、解鎖位置也是有講究的,比如只把 g_val++ 這個操作加鎖,此時程序就是 并行化 運行,線程 A 與 線程 B 都可以進入循環,但兩者需要在循環中競爭 鎖資源,只有搶到 鎖資源 的線程才能進行 g_val++,兩個線程同時競爭,相當于同時進行操作

也可以把整個 while 循環加鎖,程序就會變成 串行化,線程 A 或者 線程 B 搶到 鎖資源 后,就會不斷進行 g_val++,直到循環結束,才會把 鎖資源 讓出

理論上來說,并行化 要比 串行化 快,實際結果可以通過代碼呈現

int main()
{int n = 20000;size_t begin = clock();thread t1(Func, n);thread t2(Func, n);t1.join();t2.join();size_t end = clock();cout << "g_val: " << g_val << endl;cout << "time: " << end - begin << " ms" << endl;return 0;
}

首先來看看在 n = 20000 的情況下,并行化 耗時

注:測試性能需要在 release 模式下進行

耗時 4ms,似乎還挺快,接下來看看 串行化 耗時

串行化 只花了 2ms,比 并行化 還要快

為什么?
因為現在的程序比較簡單,while 循環內只需要進行 g_val++ 就行了,并行化中頻繁加鎖、解鎖的開銷要遠大于串行化單純的進行 while 循環

如果循環中的操作變得復雜,那么 并行化 是要比 串行化 快的,所以加鎖時選擇 并行化 還是 串行化,需要結合具體的場景進行判斷


這里為了讓兩個線程看到的是同一把鎖,將 mutex 對象定義成了一個 全局對象,其實也可以定義為 局部對象,配合 lambda 表達式 的捕捉列表捕獲 mutex 對象

int main()
{int n = 20000;int val = 0;mutex mtx; // 局部鎖對象size_t begin = clock();thread t1([&, n]()mutable->void{mtx.lock();while (n--)val++;mtx.unlock();});thread t2([&, n]()mutable->void{mtx.lock();while (n--)val++;mtx.unlock();});t1.join();t2.join();size_t end = clock();cout << "val: " << val << endl;cout << "time: " << end - begin << " ms" << endl;return 0;
}

注意: n 是傳值捕捉,如果相對其進行修改,需要使用 mutable 關鍵字取消常性

2.2.2.其他鎖類型

除了最常用的 mutex 互斥鎖C++11 中還提供了其他幾種版本

recursive_mutex 遞歸互斥鎖,這把鎖主要用來 遞歸加鎖 的場景中,可以看作 mutex 互斥鎖 的遞歸升級版,專門用在遞歸加鎖的場景中

比如在下面的代碼中,使用普通的 mutex 互斥鎖 會導致 死鎖問題,最終程序異常終止

// 普通互斥鎖
mutex mtx;void func(int n)
{if (n == 0)return;mtx.lock();n--;func(n);mtx.unlock();
}int main()
{int n = 1000;thread t1(func, n);thread t2(func, n);t1.join();t2.join();return 0;
}

為什么會出現 死鎖
因為當前在進入遞歸函數前,申請了鎖資源,進入遞歸函數后(還沒有釋放鎖資源),再次申請鎖資源,此時就會出現 鎖在我手里,但我還申請不到 的現象,也就是 死鎖

解決這個 死鎖 問題的關鍵在于 自己在持有鎖資源的情況下,不必再申請,此時就要用到 recursive_mutex 遞歸互斥鎖

// 遞歸互斥鎖
recursive_mutex mtx;

使用 recursive_mutex 遞歸互斥鎖 后,程序正常運行


timed_mutex 時間互斥鎖,這把鎖中新增了 定時解鎖 的功能,可以在程序運行指定時間后,自動解鎖(如果還沒有解鎖的話)

其中的 try_lock_for 是按照 相對時間 進行自動解鎖,而 try_lock_until 則是按照 絕對時間 進行自動解鎖

比如在下面的程序中,使用 timed_mutex 時間互斥鎖,設置為 3 秒后自動解鎖,線程獲取鎖資源后,睡眠 5 秒,即便睡眠時間還沒有到,其他線程也可以在 3 秒后獲取鎖資源,同樣進入睡眠

// 時間互斥鎖
timed_mutex mtx;void func()
{// 3秒后自動解鎖mtx.try_lock_for(chrono::seconds(3));// 睡眠5秒for (int i = 1; i <= 5; i++){this_thread::sleep_for(chrono::seconds(1));cout << "線程 " << this_thread::get_id() << " 已經睡眠了 " << i << " 秒" << endl;}mtx.unlock();
}int main()
{thread t1(func);thread t2(func);t1.join();t2.join();return 0;
}


至于最后一個 recursive_timed_mutex 遞歸時間互斥鎖,就是對 timed_mutex 時間互斥鎖 做了 遞歸 方面的升級,使其在面對 遞歸 場景時,不會出現 死鎖

2.2.3.RAII 風格的鎖

手動加鎖、解鎖可能會面臨 死鎖 問題,比如在引入 異常處理 后,如果在 臨界區 內出現了異常,程序會直接跳轉至 catch 中捕獲異常,這就導致 鎖資源 沒有被釋放,其他線程申請鎖資源時,就會出現 死鎖 問題

// 死鎖
mutex mtx;void func()
{for (int i = 0; i < 2; i++){try{mtx.lock();if (i % 2 == 0)throw exception("拋出異常");mtx.unlock();}catch (const std::exception& msg){cout << msg.what() << endl;}}
}int main()
{thread t1(func);thread t2(func);t1.join();t2.join();return 0;
}

這里引發 死鎖問題 的關鍵在于 線程在出現異常后,直接跳轉至 catch 代碼塊中,并且沒有釋放鎖資源

解決方法有兩個:

  1. catch 代碼塊中手動釋放鎖資源(不推薦)
  2. 使用 RAII 風格的鎖(推薦)

RAII 風格就是 資源獲取就是初始化 ,也就是利用對象出了作用域會自動調用析構函數這個特性,來 自動釋放鎖資源

編寫一個 LockGuard

// RAII 風格
template<class locktype>
class LockGuard
{
public:LockGuard(locktype& mtx):_mtx(mtx){// 加鎖_mtx.lock();}~LockGuard(){// 解鎖_mtx.unlock();}private:locktype& _mtx;
};

注意:

  1. 需要使用模板,因為互斥鎖有多個版本
  2. 成員變量 _mtx 需要使用引用類型,因為所有的鎖都不支持拷貝

使用引用類型作為類中的成員變量時,需要在 初始化列表 中進行初始化,以下三種類型需要在初始化列表進行初始化:

  1. 引用類型
  2. const 修飾
  3. 沒有默認構造函數的類型

修改之前的代碼,不再手動加鎖、解鎖

void func()
{for (int i = 0; i < 2; i++){try{LockGuard<mutex> lock(mtx);if (i % 2 == 0)throw exception("拋出異常");}catch (const std::exception& msg){cout << msg.what() << endl;}}
}

此時再次運行,可以發現程序正常運行,證明鎖資源被自動釋放了


其實庫中已經提供了 RAII 風格的類了,分別是 lock_guardunique_lock

其中 lock_guard 和我們自己實現的 LockGuard 幾乎一樣,功能十分簡單(構造時加鎖,析構時解鎖)

unique_lock 在此基礎上增加了一些功能,比如 加鎖、解鎖、賦值、交換 等,因為在某些場景中,需要在臨界區內對鎖資源進行操作,此時就比較適合使用 unique_lock


在使用 互斥鎖 時,推薦使用 lock_guard 或者 unique_lock 進行 自動加鎖、解鎖,避免 死鎖問題

2.3.condition_variable 條件變量類

線程安全 不僅需要 互斥鎖,還需要 條件變量條件變量 主要用來同步各線程間的信息(線程同步),同時可以避免 死鎖問題,因為如果線程條件不滿足,它就會主動將 鎖資源 讓出,讓其他線程先運行

C++11 提供了一個 condition_variable 條件變量類,其中包含了 構造、析構、等待、喚醒 相關接口

條件變量 也是不支持拷貝的,在 wait 等待時,有兩種方式:

  1. 傳統等待,傳入一個 unique_lock 對象
  2. 帶仿函數的等待,傳入一個 unique_lock 對象,以及一個返回值為 bool 的函數對象,可以根據函數對象的返回值判斷是否需要等待

為什么要在條件變量 wait 時傳入一個 unique_lock 對象?
因為條件變量本身不是線程安全的,同時在條件變量進入等待狀態時,需要有釋放鎖資源的能力,否則無法將鎖資源讓出;當條件滿足時,條件變量要有申請鎖資源的能力,以確保后續操作的線程安全,所以把互斥鎖傳給條件變量合情合理

注:使用條件變量需要包含 condition_variable 頭文件

int main()
{mutex mtx;condition_variable cond;// unique_lock 對象unique_lock<mutex> lock(mtx);// 傳統等待cond.wait(lock);// 帶函數對象的等待cond.wait(lock, []()->bool { return true; });return 0;
}

注意: 函數對象返回 true 表示條件為真,不需要等待,返回 false 表示需要等待

至于 wait_forwait_until 就是帶時間限制的等待,這里不再細談

notify_one 表示隨機喚醒一個正在等待中的線程,notify_all 表示喚醒所有正在等待中的線程,如果喚醒時,沒有線程在等待,那就什么都不會發生

條件變量 的使用看似簡單,關鍵在于如何結合具體場景進行設計

2.3.1.交替打印數字

題目要求
給你兩個線程 T1T1,要求 T1 打印奇數,T2 打印偶數,數字范圍為 [1, 10],兩個線程必須交替打印

兩個線程交替打印,并且打印的是同一個值,所以需要使用 互斥鎖 保護,由于題目要求 T1 打印奇數,T2 打印偶數,可以使用 條件變量 來判斷條件是否滿足,只有滿足才能打印,具體實現代碼如下

int main()
{mutex mtx;condition_variable cond;int n = 10;int x = 1; // 從 1 開始// 創建線程thread T1([&, n]()->void {while (x <= n){unique_lock<mutex> lock(mtx);// 避免非法情況if (x == n && n % 2 == 0)break;// 不為奇數就等待while (x % 2 != 1)cond.wait(lock); 直接這樣寫也是可以的//cond.wait(lock, [&]()->bool { return x % 2 == 1; });cout << "T1: " << x++ << endl;// 喚醒其他線程cond.notify_one();}});thread T2([&, n]()->void{while (x <= n){unique_lock<mutex> lock(mtx);// 避免非法情況if (x == n && n % 2 == 1)break;// 不為偶數,就等待while (x % 2 != 0)cond.wait(lock); 這樣寫也是可以的//cond.wait(lock, [&]()->bool {return x % 2 == 0; });cout << "T2: " << x++ << endl;// 喚醒其他線程cond.notify_one();}});T1.join();T2.join();return 0;
}

如何確保兩個線程交替打印?
某個線程在打印后,條件必定不滿足,只能 wait 等待,在這之前會喚醒另一個線程進行打印,因為數字范圍全是正數,即只有奇數和偶數兩種狀態,所以兩個線程可以相互配合、相互喚醒,從而達到交替打印的效果

如何確保打印時不會出現非法情況?
判斷待打印的數字是否符合范圍,如果不符合就不進行打印,直接 break 結束循環,因為這里是 RAII 風格的鎖,所以不必擔心死鎖問題

2.4.atomic 原子操作類

在學習 atomic 原子操作類 之前,需要先看看什么是 原子操作

原子操作 是一種 “可靠” 的操作,只允許存在 成功失敗 兩種狀態,比如對變量的修改,要么修改成功,要么修改失敗,不會存在修改一半被切走的狀態(被別人影響)

要想實現 原子操作 就得確保硬件支持 CAS(compare and swap)硬件同步原語CAS 簡單來說就是 操作前先保存舊值,準備進行操作時,取操作數的值與舊值進行比較,如果相同就進行操作,否則就更新舊值,準備重新操作

結合具體的場景理解,假設現在有一個單鏈表 list線程A 在進行尾插時,線程B 也進行了尾插,并且插入過程比 線程A 快,此時得益于 CAS線程A 發現需要連接的節點變了,也就不再進行插入,而是更新尾節點信息,重新尾插

也就是說,基于 CAS原子操作 需要確保待操作數沒有發生改變,如果被其他線程更改了,就不能進行之前的操作,而是需要更新信息后重新操作

類似的代碼實現如下(基于無鎖隊列實現的鏈表)

EnQueue(Q, data) //進隊列
{//準備新加入的結點數據n = new node();n->value = data;n->next = NULL;do {p = Q->tail; //取鏈表尾指針的快照} while( CAS(p->next, NULL, n) != TRUE); //while條件注釋:如果沒有把結點鏈在尾指針上,再試CAS(Q->tail, p, n); //置尾結點 tail = n;
}

如果只是單純的進行 i++ 操作,CAS 邏輯可以寫成這樣

int i = 0;
int old = i; // 保存舊值// 如果 CAS 函數在對 old 和 i 進行比較時,發現兩者不相等
// 就會返回 `false`,進入循環更新 `old` 舊值,準備下一次 CAS 判斷
// 直到兩者相等,才會進行操作,確保整個過程是原子的
while (!CAS(&i, old, old+1))
{old = i;
}// 進行操作
// ...

關于 CAS 的更多詳細信息可以看看 陳皓 大佬的這篇文章:《無鎖隊列的實現》


CAS 操作可以自己手搓,也可以使用庫中提供的,比如 C++11 中的 atomic 原子操作類,其中提供了一系列 原子操作,比如 加、減、位運算

借助 atomic 原子操作 類,就可以在不使用鎖的情況下,確保整型變量 g_val 的線程安全

注:使用 atomic 原子操作類需要包含 atomic 這個頭文件

// 定義為原子變量
atomic<int> g_val = 0;void Func(int n)
{while (n--)g_val++;
}int main()
{int n = 20000;thread t1(Func, n);thread t2(Func, n);t1.join();t2.join();cout << "g_val: " << g_val << endl;return 0;
}

除了整型 int 之外,atomic 還支持定義以下類型為 原子變量


atomic 定義的原子變量類型與普通變量類型并不匹配,比如使用 printf 進行打印時,就無法匹配 %d 這個格式

int main()
{// 定義為原子變量atomic<int> val = 0;printf("%d\n", val);return 0;
}

此時可以借助 atomic 類中的 load 函數,加載該原子類型的普通類型值

此時可以正常匹配

// ...
printf("%d\n", val.load());
// ...

除了 load 之外,還可以使用 store 獲取其中的值

// ...
int tmp = 0;
val.store(tmp);
printf("%d\n", tmp);
// ...

線程庫中還有一個 future 類,用于 異步編程和數據共享,并不是很常用,這里就不作介紹,使用細節可以看看這篇文章 《C++11中std::future的使用》


3.包裝器

包裝器 屬于 適配器 的一種,正如 棧和隊列 可以適配各種符合條件的容器實現一樣,包裝器 也可以適配各種類型相符的函數對象,有了 包裝器 之后,對于相似類型的多個函數的調用會變得十分方便

3.1.function 包裝器

現在我們已經學習了多種可調用的函數對象類型

  • 普通函數
  • 仿函數
  • lambda 表達式

假設這三種函數對象類型的返回值、參數均一致,用于實現不同的功能,如何將它們用同一個類型來表示?

// 普通函數
void func(int n)
{cout << "void func(int n): " << n << endl;
}// 仿函數
struct Func
{
public:void operator()(int n){cout << "void operator()(int n): " << n << endl;}
};// lambda 表達式
auto lambda = [](int n)->void{cout << "[](int n)->void: " << n << endl;};

如果 C 語言中的指針學的還可以的話,可以試試使用 函數指針 來表示這三個函數對象的類型

遺憾的是,無法直接使用 函數指針 指向 仿函數對象,也無法指向 類對象

int main()
{void(*pf)(int); // 返回值為 void,參數為 int 的函數指針pf = func;pf(10);//Func f;//pf = f(); // 無法賦值pf = lambda;pf(20);return 0;
}


C++11 中,增加了 function 包裝器 這個語法,專門用來包裝函數對象,function 包裝器 是基于 可變參數模板 實現的,原型如下

template <class Ret, class... Args>
class function<Ret(Args...)>;

其中 Ret 表示函數返回值,Args 是上文中提到的可變參數包,表示傳給函數的參數,function 模板類通過 模板特化 指明了包裝的函數對象類型

有了 function 包裝器 后,可以輕松包裝之前的三個函數對象

注:使用 function 包裝器需要包含 functional 頭文件

int main()
{// 包裝器function<void(int)> f;f = func;f(10);f = Func();f(20);f = lambda;f(30);return 0;
}

包裝器 可以結合 哈希表 使用,提前準備一批任務,根據用戶發出的不同指令來調用不同的任務,比如下面這個程序,完美地在 指令函數 之間建立了映射關系

int main()
{// 包裝了返回值為 void,參數為 void 的函數類型unordered_map<string, function<void(void)>> hash;hash["下載請求"] = []()->void { cout << "正在進行下載任務..." << endl; };hash["SQL查詢"] = []()->void { cout << "正在進行SQL查詢..." << endl; };hash["日志記錄"] = []()->void { cout << "正在記錄日志信息..." << endl; };string comm; // 指令while (cin >> comm){if (!hash.count(comm))cout << "該指令不存在,請重新輸入" << endl;elsehash[comm](); // 調用函數}return 0;
}

根據給出的指令,調用對應的函數

function 包裝器 還可以用在刷題中,比如下面這道題目中,就可以使用 包裝器運算符具體操作 之間建立映射關系,使用起來十分方便

150. 逆波蘭表達式求值

class Solution 
{
public:int evalRPN(vector<string>& tokens) {// 解題思路:操作數入棧,遇到操作符,取兩個數計算后,入棧// 建立映射關系unordered_map<string, function<int(int, int)>> hash = {{"+", [](int x, int y)->int { return x + y; } },{"-", [](int x, int y)->int { return x - y; } },{"*", [](int x, int y)->int { return x * y; } },{"/", [](int x, int y)->int { return x / y; } },};stack<int> s;for(auto str : tokens){if(str != "+" && str != "-" && str != "*" && str != "/")s.push(stoi(str));else{// 注意:先獲取 y,再獲取 xint y = s.top();s.pop();int x = s.top();s.pop();s.push(hash[str](x, y));}}return s.top();}
};

關于這道題的詳細題解可以看看這篇文章 《C++題解 | 逆波蘭表達式相關》


function 包裝器 除了可以包裝常規函數對象外,還可用于包裝 類內成員函數

包裝 靜態成員函數 很簡單,指明歸屬于哪個類就行了

class Test
{
public:Test(int n = 0):_n(n){}static void funcA(int val){cout << "static void funcA(int val): " << val << endl;}void funcB(int val){cout << "void funcB(int val): " << val * _n << endl;}private:int _n = 10;
};int main()
{// 包裝靜態函數function<void(int)> f = Test::funcA;//function<void(int)> f = &Test::funcA; // 這么寫也是可以的f(10);return 0;
}

如果包裝 非靜態成員函數 就有點麻煩了,因為 非靜態成員函數 需要借助 對象 或者 對象指針 來進行調用

解決方法是:構建 function 包裝器時,指定第一個參數為類,并且包裝時需要取地址 &

使用時則需要傳入一個 對象,此時傳入 匿名對象 或者 普通對象 都行

// 包裝非靜態函數
function<void(Test, int)> f = &Test::funcB;// 傳入匿名對象
f(Test(10), 10);// 傳入普通對象
Test t(10);
f(t, 10);

關于包裝時的參數設置問題

為什么不能設置為 類的指針,這樣能減少對象傳遞時的開銷
因為設置如果設置為指針,后續在進行調用時,就需要傳地址,如果是普通對象還好說,可以取到地址,但如果是匿名對象(右值)是無法取地址的,也就無法調用函數了

那能否設置成 類的左值引用 呢?

不行,如果是左值還好,但右值無法被左值引用接收

參數設置為 const 指針 或者 右值引用 又會導致 左值 無法正常傳遞,所以這里最理想的方案就是單純設置為 普通類類型,既能接受 左值,也能接受 右值

將參數寫成 && 不是會觸發引用折疊機制嗎,這樣不就既能接收左值,也能接收右值了?
不行,引用折疊(萬能引用)是指模板推導類型的行為,普通函數是沒有這個概念,如果普通函數既想接收左值,又想接收右值,只能重載出兩個參數不同的版本了

3.2.bind 綁定

bind 綁定 是一個函數模板,它就像一個函數包裝器(適配器),接受一個可調用對象,生成一個新的可調用對象來“適應”原對象的參數列表

bind 綁定 可以修改參數傳遞時的位置以及參數個數,生成一個可調用對象,實際調用時根據 修改 規則進行實際的函數調用,具體原型如下

template <class Fn, class... Args>
bind (Fn&& fn, Args&&... args);

fn 是傳遞的 函數對象args 是傳給函數的 可變參數包,這里使用了 萬能引用(引用折疊),使其在進行模板類型推導時,既能引用左值,也能引用右值


使用 bind綁定 改變參數傳遞順序

注:placeholders 是一個命名空間,其中的 _1_2_N 稱為占位符,分別表示函數中的第1、第2、第N個參數,直接使用就行了

void Func(int a, int b)
{cout << "void Func(int a, int b): " << a << " " << b << endl;
}int main()
{// 正常調用Func(10, 20);// 綁定生成一個可調用對象auto RFunc = bind(Func, placeholders::_2, placeholders::_1);RFunc(10, 20);return 0;
}

經過 bind 綁定 后,同樣的參數傳遞,出現了不同的調用結果

bind 的底層也是仿函數,生成一個對應的類,根據用戶指定的規則,去調用函數,比如這里經過綁定后,實際調用時,RFunc 中實際在調用 Func 傳遞的參數為 20 10

除了使用 auto 自動推導 bind 生成的可調用對象類型外,還可以使用 包裝器 來包裝出類型

// 使用包裝器包裝出類型
function<void(int, int)> RFunc = bind(Func, placeholders::_2, placeholders::_1);

bind 綁定 改變參數傳遞順序很少使用,只需要簡單了解即可

注意: 在使用 bind 綁定改變參數傳遞順序時,參與交換的參數類型,至少需要支持隱式類型轉換,否則是無法交換傳遞的


bind 綁定 還可以用來指定參數個數,比如對上面的函數 Func 進行綁定,將參數 1 始終綁定為 100,后續進行調用時,只需要傳遞一個參數

int main()
{// 使用包裝器包裝出類型auto RFunc = bind(Func, 100, placeholders::_1);RFunc(20);RFunc(10, 20);return 0;}

此時如果堅持傳遞參數,會優先使用綁定的參數,再從函數參數列表中,從左到右選擇參數進行傳遞,直到參數數量符合,比如這里第二次調用雖然傳遞了 1020,但實際調用 Func 時,RFunc 會先傳遞之前綁定的值 100 作為參數1傳遞,而 10 會作為參數2傳遞,至于 20 會被丟棄


注意: 無論綁定的是哪一個參數,占位符始終都是從 _1 開始,并且連續設置

綁定普通參數顯得沒意思,bind 綁定 參數個數用在 類的成員函數 上才舒服,比如對之前 function 包裝器 包裝 類的成員函數 代碼進行優化,直接把 類對象 這個參數綁定,調用時就不需要手動傳遞 對象

class Test
{
public:Test(int n = 0):_n(n){}static void funcA(int val){cout << "static void funcA(int val): " << val << endl;}void funcB(int val){cout << "void funcB(int val): " << val * _n << endl;}void funcC(){}private:int _n = 10;
};int main()
{function<void(int)> RFuncB = bind(&Test::funcB, Test(10), placeholders::_1);RFuncB(10);return 0;
}

除了可以綁定類對象外,也可以直接綁定 val 這個參數,亦或是兩者都綁定

// 綁定對象
function<void(Test, int)> f1 = bind(&Test::funcB, placeholders::_1, 10);
f1(Test(), 0);// 兩者都綁定
function<void(int)> f2 = bind(&Test::funcB, Test(10), 20);
f2(0);

注意: 雖然參數已經綁定了,但實際調用時,仍然需要傳遞對應函數的參數,否則無法進行函數匹配調用,當然實際傳入的參數是綁定的值,這里傳參只是為了進行匹配;并且如果不對類對象進行綁定,需要更改包裝器中的類型,調用時也需要傳入參數進行匹配


🌆總結

在這C++11系列的收尾文章中,我們深入研究了lambda表達式,為函數對象提供了快速構建的方法。接著,我們學習了標準線程庫,包括線程、互斥鎖、條件變量等,為跨平臺的多線程編程提供了強大工具。最后,通過包裝器和綁定工具,我們獲得了統一函數對象類型的新手段,使得代碼更靈活、可讀性更強,為現代C++編程提供了豐富的工具和技巧


星辰大海

相關文章推薦

C++ 進階知識

C++11『右值引用與移動語義』

C++11『基礎新特性』

C++ 哈希的應用【布隆過濾器】

C++ 哈希的應用【位圖】

C++【哈希表的完善及封裝】

C++【哈希表的模擬實現】

C++【初識哈希】

C++【一棵紅黑樹封裝 set 和 map】

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/164992.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/164992.shtml
英文地址,請注明出處:http://en.pswp.cn/news/164992.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

數據結構-深度優先搜索Java實現

目錄 一、引言二、算法步驟三、原理演示遞歸實現非遞歸實現&#xff08;使用堆棧&#xff09; 四、代碼實戰五、結論 一、引言 深度優先搜索&#xff08;DFS&#xff09;是一種在圖或樹中進行搜索的算法&#xff0c;它沿著樹的深度遍歷樹的節點&#xff0c;盡可能深的搜索樹的分…

使用C++從0到1實現人工智能神經網絡及實戰案例

引言 既然是要用C++來實現,那么我們自然而然的想到設計一個神經網絡類來表示神經網絡,這里我稱之為Net類。由于這個類名太過普遍,很有可能跟其他人寫的程序沖突,所以我的所有程序都包含在namespace liu中,由此不難想到我姓劉。在之前的博客反向傳播算法資源整理中,我列舉…

CTF-PWN-QEMU-前置知識

文章目錄 QEMU 內存管理(QEMU 如何管理某個特定 VM 的內存)MemoryRegion gpa->hpaFlatView&#xff1a;表示MR 樹對應的地址空間FlatRange&#xff1a;存儲不同MR對應的地址信息AddressSpace&#xff1a;不同類型的 MemoryRegion樹RAMBlock總體簡化圖 QEMU 設備模擬 &#x…

【Java進階開發實戰】用Java中的Base64數據加密與解密處理

簡介 ? Base64編碼,是我們程序開發中經常使用到的編碼方法。它是一種基于用64個可打印字符來表示二進制數據的表示方法。它通常用作存儲、傳輸一些二進制數據編碼方法, 也是MIME(多用途互聯網郵件擴展,主要用作電子郵件標準)中一種可打印字符表示二進制數據的常見編碼方法…

Proteus下仿真AT89C51報“串行口通信失敗,請檢查電平適配是否正確。”解決辦法

在Proteus下進行AT89C51串行口仿真時&#xff0c;如果遇到“串行口通信失敗&#xff0c;請檢查電平適配是否正確”的錯誤提示&#xff0c;以下是一些解決辦法&#xff1a; 1. 了解AT89C51和外部設備的電平要求&#xff1a; 首先&#xff0c;了解AT89C51和外部設備之間的電平…

【華為OD機試python】分班【2023 B卷|100分】

【華為OD機試】-真題 !!點這里!! 【華為OD機試】真題考點分類 !!點這里 !! 題目描述 幼兒園兩個班的小朋友在排隊時混在了一起,每位小朋友都知道自己是否與前面一位小朋友是否同班,請你幫忙把同班的小朋友找出來。 小朋友的編號為整數,與前一位小朋友同班用Y表示,不同班…

C語言——文件操作

歸納編程學習的感悟&#xff0c; 記錄奮斗路上的點滴&#xff0c; 希望能幫到一樣刻苦的你&#xff01; 如有不足歡迎指正&#xff01; 共同學習交流&#xff01; &#x1f30e;歡迎各位→點贊 &#x1f44d; 收藏? 留言?&#x1f4dd; 我輩皆凡人&#xff0c;用一生鋪就的…

C++的new / delete 與 C語言的malloc/realloc/calloc / free 的講解

在C語言中我們通常會使用malloc/realloc/calloc來動態開辟的空間&#xff0c;malloc是只會開辟你提供的空間大小&#xff0c;并不會初始化內容&#xff1b;calloc不但會開辟空間&#xff0c;還會初始化&#xff1b;realloc是專門來擴容的&#xff0c;當你第一次開辟的空間不夠用…

目標檢測YOLO實戰應用案例100講-基于YOLO的小目標檢測改進算法(續)

目錄 3.3基于混合注意力的多尺度特征融合改進方法 3.3.1整體網絡架構 3.3.2特征金字塔的構建

Vue 2.0源碼分析-實例掛載的實現

Vue 中我們是通過 $mount 實例方法去掛載 vm 的&#xff0c;$mount 方法在多個文件中都有定義&#xff0c;如 src/platform/web/entry-runtime-with-compiler.js、src/platform/web/runtime/index.js、src/platform/weex/runtime/index.js。因為 $mount 這個方法的實現是和平臺…

Python 使用tkinter復刻Windows記事本UI和菜單功能(三)

上一篇&#xff1a;Python 使用tkinter復刻Windows記事本UI和菜單功能&#xff08;二&#xff09;-CSDN博客 下一篇&#xff1a;敬請耐心等待&#xff0c;如發現BUG以及建議&#xff0c;請在評論區發表&#xff0c;謝謝&#xff01; 本文章完成了記事本的新建、保存、另存、打…

【技巧】前端開發技巧 增加前端的請求緩存 提高開發效率

定義變量 /*** 開發緩存 開關* 說明* 方便開發使用 提升開發效率* true 打開緩存* false 關閉緩存 這里上線的時候必須改為* type {boolean}*/ const cacheFlag true/*** 排除某個url 方便開發時的數據實時生效* 這里根據開發到哪個功能 實時變更&#xff0c; 比如開…

京東數據分析(京東大數據):2023年10月京東手機行業品牌銷售排行榜

鯨參謀監測的京東平臺10月份手機市場銷售數據已出爐&#xff01; 根據鯨參謀平臺的數據顯示&#xff0c;今年10月份&#xff0c;京東平臺手機行業的銷量約340萬&#xff0c;環比增長約11%&#xff0c;同比則下滑約2%&#xff1b;銷售額為108億&#xff0c;環比增長約17%&#x…

請你說一下Vue中v-if和v-for的優先級誰更高

v-if 與 v-for簡介 v-ifv-forv-if & v-for使用 v-if 與 v-for優先級比較 vue2 中&#xff0c;v-for的優先級高于v-if 例子進行分析 vue3 v-if 具有比 v-for 更高的優先級 例子進行分析 總結 在vue2中&#xff0c;v-for的優先級高于v-if在vue3中&#xff0c;v-if的優先級高…

RubyMine 2023:提升Rails/Ruby開發效率的強大利器

在Rails/Ruby開發領域&#xff0c;JetBrains RubyMine一直以其強大的功能和優秀的性能而備受開發者的青睞。現如今&#xff0c;我們迎來了全新的RubyMine 2023版本&#xff0c;它將為開發者們帶來更高效的開發體驗和無可比擬的工具支持。 首先&#xff0c;RubyMine 2023提供了…

Java-使用poi-tl根據word模板動態生成word

作者wangsz&#xff0c;想寫一些關于word的工具&#xff0c;所以就寫了這篇文章 1.首先&#xff0c;先導入所需要的依賴&#xff08;poi相關依賴即可&#xff09; <!-- POI --><dependency><groupId>org.apache.poi</groupId><artifactId>poi&l…

【libGDX】使用Mesh繪制立方體

1 前言 本文主要介紹使用 Mesh 繪制立方體&#xff0c;讀者如果對 Mesh 不太熟悉&#xff0c;請回顧以下內容&#xff1a; 使用Mesh繪制三角形使用Mesh繪制矩形使用Mesh繪制圓形 在繪制立方體的過程中&#xff0c;主要用到了 MVP &#xff08;Model View Projection&#xff0…

目標檢測YOLO系列從入門到精通技術詳解100篇-【目標檢測】計算機視覺(最終篇)

目錄 知識儲備 KITTI數據集 1.KITTI數據集概述 2.數據采集平臺 3.Dataset詳述 算法原理

GIT無效的源路徑/URL

ssh-add /Users/haijunyan/.ssh/id_rsa ssh-add -K /Users/haijunyan/.ssh/id_rsa

windows11上enable WSL

Windows電腦上要配置linux&#xff08;這里指ubuntu&#xff09;開發環境&#xff0c;主要有三種方式&#xff1a; 1&#xff09;在windows上裝個虛擬機&#xff08;比如vmware&#xff09;。缺點是vmware加載ubuntu后系統會變慢很多&#xff0c;而且需要通過samba來實現window…