C++11從可變參數(shù)模板到bind綁定器的詳細(xì)過(guò)程
前言:書(shū)接上回
在上一篇文章《C++11初探:C++發(fā)展史上的里程碑》中,我們從C++11的歷史背景出發(fā),一路攻克了統(tǒng)一初始化、右值引用、移動(dòng)語(yǔ)義、萬(wàn)能引用和完美轉(zhuǎn)發(fā)。這些特性已經(jīng)讓你手中的C++脫胎換骨。
這篇文章我們繼續(xù)介紹C++11中的三個(gè)趁手工具:
- 可變參數(shù)模板:“模板的模板”,避免繁瑣的函數(shù)重載
- lambda 表達(dá)式:簡(jiǎn)單好用的可調(diào)用對(duì)象
- function + bind:包裝可調(diào)用對(duì)象的工具
每一招都能讓你的代碼更加簡(jiǎn)潔清晰。今天我們就來(lái)徹底拿下它們。
一、可變參數(shù)模板
1.1 函數(shù)重載的繁冗
假設(shè)你想寫(xiě)一個(gè)打印函數(shù),能打印任意數(shù)量的參數(shù):
// 一個(gè)參數(shù)
void Print(int x) { cout << x << endl; }
// 兩個(gè)參數(shù)——再寫(xiě)一個(gè)
void Print(int x, string y) { cout << x << " " << y << endl; }
// 三個(gè)參數(shù)——再再寫(xiě)一個(gè)
void Print(int x, string y, double z) {
cout << x << " " << y << " " << z << endl;
}
// ... 四個(gè)?五個(gè)?到底寫(xiě)到什么時(shí)候?這種寫(xiě)法不僅丑陋,而且只能支持特定類型的組合。如果用戶想打印 double + string + int 呢?你又要寫(xiě)一個(gè)重載。
C++98就這樣忍了十幾年。模板的發(fā)明解決了一部分問(wèn)題(類型可以參數(shù)化了),但參數(shù)的個(gè)數(shù)還是寫(xiě)死的。
1.2 可變參數(shù)模板的出現(xiàn)
C++11引入了可變參數(shù)模板(Variadic Templates),允許模板接受任意數(shù)量、任意類型的參數(shù):
template <class ...Args>
void Print(Args... args) {
// args 是一個(gè)"參數(shù)包"(parameter pack)
// 包含了你傳進(jìn)來(lái)的所有參數(shù)
}兩個(gè)關(guān)鍵的語(yǔ)法點(diǎn):
| 語(yǔ)法 | 位置 | 含義 |
|---|---|---|
class ...Args | 模板參數(shù)列表 | 聲明一個(gè)"類型參數(shù)包",Args不是單個(gè)類型,是一組類型 |
Args... args | 函數(shù)參數(shù)列表 | 聲明一個(gè)"函數(shù)參數(shù)包",args不是單個(gè)參數(shù),是一組參數(shù) |
類比理解:Args... 就像一個(gè)可以裝任意多個(gè)類型的"麻袋";args 就像是這個(gè)麻袋里對(duì)應(yīng)的實(shí)際參數(shù)。
1.3sizeof...(args)——數(shù)數(shù)到底有幾個(gè)
sizeof...(args) 可以獲取參數(shù)包中參數(shù)的數(shù)量,注意它返回的是編譯期常量:
template <class ...Args>
void Print(Args&&... args) {
cout << "參數(shù)個(gè)數(shù):" << sizeof...(args) << endl;
}
int main() {
Print(); // 輸出:參數(shù)個(gè)數(shù):0
Print(1); // 輸出:參數(shù)個(gè)數(shù):1
Print(1, "hello"); // 輸出:參數(shù)個(gè)數(shù):2
Print(1, 2.5, "hello"); // 輸出:參數(shù)個(gè)數(shù):3
}好,現(xiàn)在我們能"數(shù)"參數(shù)了,但真正的問(wèn)題是——怎么取出每一個(gè)參數(shù)來(lái)用?
1.4 參數(shù)包的展開(kāi)
坑1:不能用 [] 下標(biāo)訪問(wèn)
初學(xué)者最容易想到的寫(xiě)法——編譯不通過(guò)!:
template <class ...Args>
void Print(Args&&... args) {
// 錯(cuò)誤示范!不支持下標(biāo)訪問(wèn)
for (size_t i = 0; i < sizeof...(args); i++) {
cout << args[i] << " "; // ? 編譯錯(cuò)誤!
}
cout << endl;
}原因很簡(jiǎn)單:sizeof...(args) 雖然是編譯期常量,但 args[i] 這種下標(biāo)訪問(wèn)語(yǔ)法只適用于同類型的數(shù)組。而 args 里每個(gè)參數(shù)的類型都可能不一樣(一個(gè)int,一個(gè)string,一個(gè)double……),編譯器根本無(wú)法確認(rèn) args[i] 的類型!
坑2:不支持范圍for循環(huán)
template <class ...Args>
void Print(Args&&... args) {
// 錯(cuò)誤示范!范圍for不支持參數(shù)包展開(kāi)
for (auto&& e : args...) { // ? 同樣編譯不通過(guò)
cout << e << " ";
}
cout << endl;
}
范圍for要求 args 是一個(gè)可迭代的容器,但參數(shù)包不是容器,編譯器不知道怎么遍歷它。
1.5 方法一:遞歸展開(kāi)(最經(jīng)典的方式)
可變參數(shù)包的"正統(tǒng)"展開(kāi)方式是遞歸——每次取出第一個(gè)參數(shù),然后把剩下的參數(shù)遞歸傳給下一層:
// 遞歸終止函數(shù)——當(dāng)參數(shù)包為空時(shí)調(diào)用
void ShowList() {
cout << endl; // 打印最后一個(gè)換行,遞歸結(jié)束
}
// 遞歸展開(kāi)函數(shù)——每次取出第一個(gè)參數(shù)x,剩下的繼續(xù)展開(kāi)
template <class T, class ...Args>
void ShowList(T&& x, Args&&... args) {
cout << x << " "; // 處理當(dāng)前層的第一個(gè)參數(shù)
ShowList(args...); // 剩下的參數(shù)包遞歸傳給下一層
}
// 對(duì)外接口——直接轉(zhuǎn)發(fā)給實(shí)際的處理函數(shù)
template <class ...Args>
void Print(Args&&... args) {
ShowList(args...);
}
int main() {
Print(1, 2.5, string("hello"), 'A');
// 輸出:1 2.5 hello A
return 0;
}來(lái)看看這段代碼是怎么一層層展開(kāi)的:
調(diào)用 Print(1, 2.5, "hello", 'A')
↓
調(diào)用 ShowList(1, 2.5, "hello", 'A')
x = 1, 打印 "1 "
args = (2.5, "hello", 'A')
↓ 遞歸
調(diào)用 ShowList(2.5, "hello", 'A')
x = 2.5, 打印 "2.5 "
args = ("hello", 'A')
↓ 遞歸
調(diào)用 ShowList("hello", 'A')
x = "hello", 打印 "hello "
args = ('A')
↓ 遞歸
調(diào)用 ShowList('A')
x = 'A', 打印 "A "
args = () ← 空參數(shù)包
↓ 遞歸
調(diào)用 ShowList() ← 匹配到終止函數(shù),打印換行關(guān)鍵點(diǎn):
- 每次遞歸參數(shù)減少一個(gè),直到參數(shù)包為空
- 終止函數(shù)
ShowList()是必須的,否則當(dāng)參數(shù)包為空時(shí)編譯器找不到匹配的函數(shù)。另外,你可能會(huì)想到在ShowList的函數(shù)體中寫(xiě)遞歸終止條件,這是不可行的,因?yàn)镃++ 模板參數(shù)包是編譯期語(yǔ)法,不能用運(yùn)行時(shí) if 判斷參數(shù)包是否為空。 - 這里用了
T&&萬(wàn)能引用,配合引用折疊,完美接收各種值類別
1.6 利用函數(shù)參展開(kāi)
遞歸展開(kāi)雖然經(jīng)典,但需要寫(xiě)兩個(gè)函數(shù),稍顯啰嗦。如果你只是想把參數(shù)包里的每個(gè)元素"過(guò)一遍"(比如打印),可以利用函數(shù)參數(shù)來(lái)展開(kāi):
template <class T>
int GetArg(const T& x) {
cout << x << " ";
return 0;
}
// 這個(gè)函數(shù)什么都不做,只是用來(lái)"容納"逗號(hào)表達(dá)式展開(kāi)的結(jié)果
template <class ...Args>
void Arguments(Args... args) {}
template <class ...Args>
void Print(Args... args) {
// 核心魔法在這一行!
Arguments(GetArg(args)...);
}
int main() {
double x = 2.2;
Print();
Print(1);
Print(1, string("xxxxx"));
Print(1.1, string("xxxxx"), x);
return 0;
}等一下,這一行到底發(fā)生了什么?
Arguments(GetArg(args)...);
這個(gè)語(yǔ)法 GetArg(args)... 叫做參數(shù)包展開(kāi),注意 ... 的位置——它在 GetArg(args) 的外面。意思是:把參數(shù)包中的每個(gè)參數(shù)依次傳給 GetArg,得到一組返回值,再把這組返回值作為 Arguments 的實(shí)參。
舉個(gè)例子,Print(1, string("hello"), 2.2) 展開(kāi)后等價(jià)于:
Arguments(GetArg(1), GetArg(string("hello")), GetArg(2.2));GetArg 每次被調(diào)用都會(huì)打印一個(gè)參數(shù)并返回0(返回值其實(shí)無(wú)所謂,只是為了湊夠傳給 Arguments 的實(shí)參)。而 Arguments 什么都不做,它的唯一作用就是——作為一個(gè)"接收器",讓逗號(hào)表達(dá)式能夠"合法"地展開(kāi)。
1.7 可變參數(shù)模板 + 完美轉(zhuǎn)發(fā)
現(xiàn)在我們把可變參數(shù)模板和上一篇文章學(xué)的完美轉(zhuǎn)發(fā)結(jié)合起來(lái)。
看看 STL 中 list 容器的節(jié)點(diǎn)構(gòu)造函數(shù):
template<class T>
struct list_node {
T _data;
list_node<T>* _next;
list_node<T>* _prev;
list_node() = default;
// 可變參數(shù) + 萬(wàn)能引用 + 完美轉(zhuǎn)發(fā) → 三合一
template <class... Args>
list_node(Args&&... args)
: _next(nullptr)
, _prev(nullptr)
, _data(std::forward<Args>(args)...) // 注意這里...
{}
};注意 std::forward<Args>(args)... 這行,同樣要把 ... 放在外面,表示"對(duì)參數(shù)包里的每個(gè)參數(shù)都執(zhí)行 std::forward"。展開(kāi)后類似:
_data(std::forward<T1>(arg1), std::forward<T2>(arg2), std::forward<T3>(arg3))
這樣就能把每個(gè)參數(shù)以及它的"左值/右值"屬性原封不動(dòng)地轉(zhuǎn)發(fā)給 _data 的構(gòu)造函數(shù)。
二、emplace_back 如何碾壓 push_back
2.1 回顧 push_back 的兩種重載
在深挖 emplace_back 之前,先回顧一下我們學(xué)過(guò)的 push_back:
template<class T>
class list {
public:
// 左值引用版本:深拷貝
void push_back(const T& x) {
insert(end(), x);
}
// 右值引用版本:移動(dòng)
void push_back(T&& x) {
insert(end(), forward<T>(x));
}
};調(diào)用場(chǎng)景:
jys::list<jys::string> lt;
jys::string s1("111111111111");
lt.push_back(s1); // s1是左值 → 調(diào)用 const T& 版本
lt.push_back(move(s1)); // move后是右值 → 調(diào)用 T&& 版本
lt.push_back("111111111111"); // 字面量是右值 → 調(diào)用 T&& 版本,但需要隱式構(gòu)造string注意第三種情況:push_back("111111111111") 雖然是右值,但 需要先用 "111111111111" 構(gòu)造一個(gè)臨時(shí)的 string 對(duì)象,再把這個(gè)臨時(shí)對(duì)象移動(dòng)到 list 節(jié)點(diǎn)中。多了一步隱式構(gòu)造+移動(dòng)。
2.2 emplace_back:直接節(jié)點(diǎn)中原地構(gòu)造
emplace_back 的核心優(yōu)勢(shì)在于直接在節(jié)點(diǎn)的內(nèi)存空間上構(gòu)造對(duì)象,徹底消滅了"先構(gòu)造再拷貝/移動(dòng)"的中間步驟。
template<class T>
class list {
public:
// emplace_back 接受任意參數(shù),直接轉(zhuǎn)發(fā)給節(jié)點(diǎn)構(gòu)造函數(shù)
template <class... Args>
void emplace_back(Args&&... args) {
insert(end(), std::forward<Args>(args)...);
}
template <class... Args>
iterator insert(iterator pos, Args&&... args) {
Node* cur = pos._node;
Node* prev = cur->_prev;
// new Node時(shí)直接把參數(shù)轉(zhuǎn)發(fā)給T的構(gòu)造函數(shù),在堆上原地構(gòu)造
Node* newnode = new Node(std::forward<Args>(args)...);
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
};來(lái)看看對(duì)比——同樣插入一個(gè) jys::string:
int main() {
list<jys::string> lt;
jys::string s1("111111111111");
jys::string s2("222222222222");
// 場(chǎng)景1:傳左值
lt.push_back(s1); // const T& → 調(diào)string的拷貝構(gòu)造
lt.emplace_back(s1); // 同樣傳左值 → 也調(diào)拷貝構(gòu)造,和push_back沒(méi)區(qū)別
// 場(chǎng)景2:傳右值
lt.push_back(move(s2)); // T&& → 調(diào)string的移動(dòng)構(gòu)造
lt.emplace_back(move(s1)); // 同樣傳右值 → 也調(diào)移動(dòng)構(gòu)造,和push_back沒(méi)區(qū)別
// 場(chǎng)景3:直接傳構(gòu)造參數(shù)——這是emplace_back真正的舞臺(tái)!
lt.emplace_back("111111111111"); // 直接把const char*傳給節(jié)點(diǎn)的string構(gòu)造
// 在節(jié)點(diǎn)內(nèi)部原地構(gòu)造,一次構(gòu)造搞定!
cout << "***********************" << endl;
lt.push_back("111111111111"); // 先用const char*構(gòu)造臨時(shí)string
// 再把臨時(shí)string移動(dòng)到節(jié)點(diǎn)中
// 兩步操作:構(gòu)造+移動(dòng)(有可能被優(yōu)化掉)
return 0;
}emplace_back 真正的優(yōu)勢(shì)在場(chǎng)景3:當(dāng)你可以直接傳遞構(gòu)造函數(shù)的參數(shù)時(shí),emplace_back 避免了"先構(gòu)造臨時(shí)對(duì)象再拷貝/移動(dòng)"的額外開(kāi)銷。
2.3 pair 的場(chǎng)景對(duì)比——差異非常明顯
對(duì) pair 這種復(fù)合類型,emplace_back 的威力更加直觀:
int main() {
jys::list<pair<jys::string, int>> lt1;
// ===== push_back 方式 =====
pair<jys::string, int> kv("蘋果", 1);
lt1.push_back(kv); // pair的拷貝構(gòu)造 → 內(nèi)部string的拷貝構(gòu)造
cout << "***********************" << endl;
lt1.push_back(move(kv)); // pair的移動(dòng)構(gòu)造 → 內(nèi)部string的移動(dòng)構(gòu)造
cout << "***********************" << endl;
lt1.push_back({"蘋果", 1}); // 先構(gòu)造臨時(shí)pair → 再移動(dòng)到節(jié)點(diǎn)
cout << "***********************" << endl;
// ===== emplace_back 方式 =====
// 直接傳兩個(gè)參數(shù)給pair的構(gòu)造函數(shù),在節(jié)點(diǎn)內(nèi)部原地構(gòu)造!
lt1.emplace_back("蘋果", 1);
// 注意:不能寫(xiě)成 emplace_back({"蘋果", 1}),因?yàn)閧}不會(huì)被推導(dǎo)為pair
cout << "***********************" << endl;
return 0;
}關(guān)鍵區(qū)別:
| 方法 | 對(duì) pair<string,int> 的操作 | 開(kāi)銷 |
|---|---|---|
| push_back(kv) | 拷貝構(gòu)造pair → 內(nèi)部string拷貝 | 深拷貝,開(kāi)銷最大 |
| push_back(move(kv)) | 移動(dòng)構(gòu)造pair → 內(nèi)部string移動(dòng) | 較輕,但仍有移動(dòng) |
| push_back({"蘋果", 1}) | 隱式構(gòu)造臨時(shí)pair + 移動(dòng) | 構(gòu)造一次 + 移動(dòng)一次 |
| emplace_back("蘋果", 1) | 直接原地構(gòu)造pair | 構(gòu)造一次,沒(méi)有額外開(kāi)銷 |
總結(jié):當(dāng)你需要往容器里加新元素且可以直接給出構(gòu)造參數(shù)時(shí),用 emplace_back;當(dāng)你已經(jīng)有一個(gè)現(xiàn)成的對(duì)象時(shí),emplace_back 和 push_back 沒(méi)有本質(zhì)區(qū)別。
三、lambda 表達(dá)式:把函數(shù)寫(xiě)成一行的藝術(shù)
3.1 從仿函數(shù)的痛苦說(shuō)起
在 lambda 出現(xiàn)之前,C++98 里要實(shí)現(xiàn)一個(gè)簡(jiǎn)單的比較邏輯,你得這么寫(xiě):
struct Goods {
string _name;
double _price;
int _evaluate;
Goods(const char* str, double price, int evaluate)
: _name(str), _price(price), _evaluate(evaluate) {}
};
// 按價(jià)格升序比較——得寫(xiě)一個(gè)完整的仿函數(shù)類
struct ComparePriceLess {
bool operator()(const Goods& gl, const Goods& gr) {
return gl._price < gr._price;
}
};
// 按價(jià)格降序比較——又得寫(xiě)一個(gè)
struct ComparePriceGreater {
bool operator()(const Goods& gl, const Goods& gr) {
return gl._price > gr._price;
}
};
int main() {
vector<Goods> v = {
{"蘋果", 2.1, 5}, {"香蕉", 3, 4},
{"橙子", 2.2, 3}, {"菠蘿", 1.5, 4}
};
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
return 0;
}每換一個(gè)排序維度(價(jià)格↔評(píng)價(jià))、每換一個(gè)方向(升序↔降序),都要新寫(xiě)一個(gè)仿函數(shù)類。放在代碼里占位置不說(shuō),邏輯還和調(diào)用點(diǎn)離得老遠(yuǎn)。
3.2 lambda :一行代碼,所見(jiàn)即所得
C++11 的 lambda 表達(dá)式就是為這種場(chǎng)景而生的:
int main() {
vector<Goods> v = {
{"蘋果", 2.1, 5}, {"香蕉", 3, 4},
{"橙子", 2.2, 3}, {"菠蘿", 1.5, 4}
};
// 價(jià)格升序——比較邏輯就寫(xiě)在調(diào)用處!
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._price < g2._price;
});
// 價(jià)格降序——改一個(gè)符號(hào)就行
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._price > g2._price;
});
// 按評(píng)價(jià)排序——換個(gè)成員變量就行
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate < g2._evaluate;
});
return 0;
}優(yōu)點(diǎn)一目了然:定義即使用,不需要起名字,不需要跳到別的地方看代碼。這就是 lambda 的表達(dá)力。
3.3 lambda 語(yǔ)法詳解
lambda 表達(dá)式的完整語(yǔ)法如下:
[捕獲列表] (參數(shù)列表) mutable -> 返回值類型 { 函數(shù)體 }
四個(gè)組成部分:
| 部分 | 說(shuō)明 | 可否省略 |
|---|---|---|
[捕獲列表] | 捕獲外部變量 | 不可省略(可以為空 []) |
(參數(shù)列表) | 形參列表 | 可以省略(無(wú)參時(shí)) |
mutable | 允許修改值捕獲的變量(修改的是副本) | 可以省略 |
-> 返回值類型 | 返回類型 | 可以省略(編譯器自動(dòng)推導(dǎo)) |
{ 函數(shù)體 } | 函數(shù)體 | 不可省略 |
最精簡(jiǎn)的 lambda:
auto func1 = [] {
cout << "hello jys" << endl;
return 0;
};
func1(); // 調(diào)用注意:這里 [] 是捕獲列表(空的),{} 是函數(shù)體——其他全省略了。
帶參數(shù)和返回值的 lambda:
auto add1 = [](int x, int y) -> int { return x + y; };
cout << add1(1, 2) << endl; // 輸出: 3
// -> int 可以省略,編譯器從 return 語(yǔ)句自動(dòng)推導(dǎo)返回值類型
auto add2 = [](int x, int y) { return x + y; };
cout << add2(3, 4) << endl; // 輸出: 73.4 捕獲列表——lambda 最精妙的設(shè)計(jì)
lambda 的重點(diǎn)和難點(diǎn)都在于捕獲列表。它決定了 lambda 內(nèi)部可以訪問(wèn)哪些外部變量。
值捕獲[變量名]
捕獲外部變量的副本,lambda 內(nèi)部的修改不影響外部變量:
int a = 0, b = 1, c = 2;
auto func = [a, b](int x) mutable {
a++; // 修改的是a的副本,外部a不變
b++; // 修改的是b的副本,外部b不變
return a + b + x;
};
cout << func(1) << endl; // 內(nèi)部 a=1, b=2, x=1 → 輸出4
cout << "外部 a=" << a << endl; // 輸出:外部 a=0 (沒(méi)變?。?/pre>注意:
mutable關(guān)鍵字是必須的,否則值捕獲的變量在 lambda 內(nèi)是 const 的,不能修改- 值捕獲發(fā)生在 lambda 定義時(shí),而不是調(diào)用時(shí)
引用捕獲[&變量名]
捕獲外部變量的引用,lambda 內(nèi)部的修改直接影響外部變量:
int a = 0, b = 1;
auto func = [&a, &b] {
a++; // 修改的就是外部a
b++; // 修改的就是外部b
};
func();
cout << "a=" << a << ", b=" << b << endl; // 輸出:a=1, b=2隱式捕獲[=]和[&]
如果 lambda 里要用很多外部變量,一個(gè)個(gè)寫(xiě)捕獲列表太麻煩:
int a = 0, b = 1, c = 2, d = 3;
// [=] 以值的方式捕獲所有用到的外部變量
auto func1 = [=] {
return a + b + c; // a, b, c 都以值方式捕獲
};
// [&] 以引用的方式捕獲所有用到的外部變量
auto func2 = [&] {
a++; c++; // a, c 都以引用方式捕獲,修改直接影響外部
};
func2();
cout << "a=" << a << ", c=" << c << endl; // 輸出:a=1, c=3混合捕獲[=, &變量]和[&, 變量]
還可以混合使用,實(shí)現(xiàn)"大部分用一種方式,個(gè)別變量例外":
int a = 0, b = 1, c = 2, d = 3;
// 大部分值捕獲,但 c 和 d 用引用捕獲
auto func4 = [=, &c, &d] {
// a++; b++; // ? 值捕獲,不能修改(除非加mutable)
c++; // ? 引用捕獲,可以修改
d++; // ? 引用捕獲,可以修改
return a + b + c + d;
};
func4();
cout << a << " " << b << " " << c << " " << d << endl;
// 輸出:0 1 3 4(c和d被修改了)
// 大部分引用捕獲,但 a 和 b 用值捕獲
auto func5 = [&, a, b] {
// a++; // ? 值捕獲,不能修改
c++; // ? 引用捕獲
d++; // ? 引用捕獲
};全局變量:不需要捕獲
全局變量在 lambda 中可以直接訪問(wèn),不用捕獲:
int y = 0; // 全局變量
auto func = [] {
y++; // 直接訪問(wèn),不需要捕獲
};3.5 lambda 的本質(zhì)——編譯器幫你寫(xiě)的仿函數(shù)
你有沒(méi)有想過(guò),lambda 到底是個(gè)什么類型?
auto func = [](int x, int y) { return x + y; };
cout << typeid(func).name() << endl; // 輸出一串編譯器生成的類名
lambda 的本質(zhì)是編譯器自動(dòng)生成的匿名仿函數(shù)類。編譯器看到了你的 lambda 表達(dá)式,就默默幫你生成了一個(gè)結(jié)構(gòu)類似下面的類:
// 你看不到這個(gè)類,但編譯器在背后就是這么干的
class __lambda_unique_id {
public:
// []的捕獲內(nèi)容對(duì)應(yīng)這里的成員變量
// ()的參數(shù)列表對(duì)應(yīng) operator() 的形參
// {}的函數(shù)體對(duì)應(yīng) operator() 的函數(shù)體
int operator()(int x, int y) const {
return x + y;
}
};再看一個(gè)有捕獲的 lambda 和對(duì)應(yīng)的仿函數(shù)對(duì)比:
double rate = 0.49;
// lambda 版本
auto r2 = [rate](double money, int year) {
return money * rate * year;
};
// 等價(jià)的仿函數(shù)版本
class Rate {
public:
Rate(double rate) : _rate(rate) {}
double operator()(double money, int year) {
return money * _rate * year;
}
private:
double _rate;
};
Rate r1(rate);
// 兩者用法完全一樣
cout << r1(10000, 2) << endl; // 仿函數(shù)
cout << r2(10000, 2) << endl; // lambdarate 在 lambda 中對(duì)應(yīng)值捕獲,在仿函數(shù)中對(duì)應(yīng)成員變量 _rate——兩者在底層是完全等價(jià)的操作。
四、新的類功能
C++11 新增了兩個(gè)默認(rèn)成員函數(shù):移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值運(yùn)算符重載。不過(guò),想讓它們自動(dòng)生成的條件十分嚴(yán)苛:
- 如果你沒(méi)有自己實(shí)現(xiàn)移動(dòng)構(gòu)造函數(shù),且沒(méi)有實(shí)現(xiàn)析構(gòu)函數(shù) 、拷貝構(gòu)造、拷貝賦值重載中的任意一個(gè),那么編譯器會(huì)自動(dòng)生成一個(gè)默認(rèn)移動(dòng)構(gòu)造。
- 如果你沒(méi)有自己實(shí)現(xiàn)移動(dòng)賦值重載函數(shù),且沒(méi)有實(shí)現(xiàn)析構(gòu)函數(shù) 、拷貝構(gòu)造、拷貝賦值重載中的任意一,那么編譯器會(huì)自動(dòng)生成一個(gè)默認(rèn)移動(dòng)賦值。
另外,如果你提供了移動(dòng)構(gòu)造或者移動(dòng)賦值,編譯器不會(huì)自動(dòng)提供拷貝構(gòu)造和拷貝賦值。
C++11 之前,如果你定義了一個(gè)構(gòu)造函數(shù),編譯器就不再生成默認(rèn)構(gòu)造函數(shù)。有些時(shí)候你明明想讓編譯器再幫你生成,卻沒(méi)法說(shuō)——直到 = default 的出現(xiàn):
#include <string>
#include <iostream>
using namespace std;
namespace jys {
class string {
// ... (自定義string類的實(shí)現(xiàn))
};
}
class Person {
public:
Person(const char* name = "張三", int age = 1)
: _name(name), _age(age) {}
// 我自定義了拷貝構(gòu)造和移動(dòng)構(gòu)造,但還想讓編譯器生成默認(rèn)版本
Person(Person&& p) = default; // 編譯器,幫我生成默認(rèn)移動(dòng)構(gòu)造
//Person(const Person& p) = delete;限制默認(rèn)函數(shù)的生成
// 析構(gòu)函數(shù)也用默認(rèn)的
~Person() = default;
private:
jys::string _name;
int _age;
};
int main() {
Person s1;
Person s2 = s1; // 調(diào)用默認(rèn)拷貝構(gòu)造(逐成員拷貝)
Person s3 = std::move(s1); // 調(diào)用默認(rèn)移動(dòng)構(gòu)造(逐成員移動(dòng))
return 0;
}= default 只能在類內(nèi)部聲明時(shí)使用,作用是顯式要求編譯器生成該函數(shù)的默認(rèn)版本。另外,如以上代碼所示,如果想要抑制默認(rèn)函數(shù)生成,在函數(shù)聲明后面加上=delete即可。
五、std::function——統(tǒng)一的可調(diào)用對(duì)象包裝器
5.1 可調(diào)用對(duì)象
C++ 里有好幾種"可以被調(diào)用"的東西:
- 普通函數(shù)
- 仿函數(shù)(重載了
operator()的類對(duì)象) - lambda 表達(dá)式
- 成員函數(shù)(需要通過(guò)對(duì)象或指針調(diào)用)
每種東西的類型都不一樣,沒(méi)法用一種統(tǒng)一的類型來(lái)聲明和傳遞。比如你想寫(xiě)一個(gè)函數(shù),接受"任意的可調(diào)用對(duì)象"作為參數(shù)——在 C++98 里只能通過(guò)模板,沒(méi)法用統(tǒng)一的類型。
5.2function:一碗水端平
std::function 是一個(gè)類模板,可以把各種可調(diào)用對(duì)象包裝成同一種類型。它定義在 <functional> 頭文件中。
#include <functional>
#include <iostream>
#include <map>
#include <string>
using namespace std;
int f(int a, int b) {
return a + b;
}
struct Functor {
int operator()(int a, int b) {
return a + b;
}
};
int main() {
// function<int(int, int)> 表示 "接受兩個(gè)int,返回int"的可調(diào)用對(duì)象
function<int(int, int)> f1 = f; // 包裝普通函數(shù)
function<int(int, int)> f2 = Functor(); // 包裝仿函數(shù)
function<int(int, int)> f3 = [](int a, int b) { // 包裝lambda
return a + b;
};
cout << f1(1, 1) << endl; // 2
cout << f2(2, 2) << endl; // 4
cout << f3(3, 3) << endl; // 6
return 0;
}看看是不是很神奇?三種來(lái)源完全不同的東西,包裝之后得到的 f1、f2、f3 的類型完全一樣——都是 function<int(int, int)>。
5.3 包裝成員函數(shù)——多了一點(diǎn)"綁定"的味道
成員函數(shù)和非成員函數(shù)不同,它們有一個(gè)隱藏的 this 指針參數(shù)。所以用 function 包裝成員函數(shù)時(shí),需要在類型簽名里多一個(gè)參數(shù):
class Plus {
public:
Plus(int n = 10) : _n(n) {}
static int plusi(int a, int b) {
return a + b;
}
double plusd(double a, double b) {
return (a + b) * _n;
}
private:
int _n;
};
int main() {
// 靜態(tài)成員函數(shù) → 和普通函數(shù)一樣包裝,不需要額外對(duì)象參數(shù)
function<int(int, int)> f4 = &Plus::plusi;
cout << f4(1, 1) << endl; // 2
// 普通成員函數(shù) → 第一個(gè)參數(shù)是調(diào)用它的對(duì)象(用指針)
function<double(Plus*, double, double)> f5 = &Plus::plusd;
Plus pl;
cout << f5(&pl, 1.1, 1.1) << endl; // 通過(guò)指針調(diào)用
// 也可以用對(duì)象值傳遞
function<double(Plus, double, double)> f6 = &Plus::plusd;
cout << f6(pl, 1.1, 1.1) << endl; // pl以值方式傳入
cout << f6(Plus(), 1.1, 1.1) << endl; // 臨時(shí)對(duì)象也可以
// 甚至可以用右值引用
function<double(Plus&&, double, double)> f7 = &Plus::plusd;
cout << f7(move(pl), 1.1, 1.1) << endl;
cout << f7(Plus(), 1.1, 1.1) << endl;
return 0;
}幾點(diǎn)需要留意的地方:
&不能?。?code>&Plus::plusi /&Plus::plusd獲取成員函數(shù)地址時(shí),&是必須的(這是語(yǔ)法要求,和獲取函數(shù)指針時(shí)的&f不同,函數(shù)名的&可以省略但成員函數(shù)的不行)- 靜態(tài)成員函數(shù) = 普通函數(shù):沒(méi)有隱含的
this指針,所以包裝簽名和非成員函數(shù)完全一樣 - 成員函數(shù)的第一個(gè)參數(shù)是
this:需要顯式在function的簽名中聲明,類型可以是類名*、類名或類名&&
5.4map + function:實(shí)現(xiàn)策略模式
function 的一個(gè)經(jīng)典應(yīng)用場(chǎng)景是把函數(shù)映射表(策略模式的一種實(shí)現(xiàn)):
int main() {
// 字符串 → 操作函數(shù)的映射表
map<string, function<int(int, int)>> opFuncMap = {
{"+", [](int x, int y) { return x + y; }},
{"-", [](int x, int y) { return x - y; }},
{"*", [](int x, int y) { return x * y; }},
{"/", [](int x, int y) { return x / y; }},
{"&", [](int x, int y) { return x & y; }},
{"|", [](int x, int y) { return x | y; }},
{"^", [](int x, int y) { return x ^ y; }}
};
// 使用起來(lái)極其方便
cout << opFuncMap["+"](3, 5) << endl; // 8
cout << opFuncMap["*"](3, 5) << endl; // 15
cout << opFuncMap["^"](6, 3) << endl; // 5
return 0;
}這種寫(xiě)法的好處:
- 增加新操作只需在 map 中添加一行,不需要修改任何其他代碼
- 調(diào)用方完全不知道底層是函數(shù)指針、仿函數(shù)還是 lambda——全部統(tǒng)一為
function<int(int, int)> - 比
switch-case或一大串if-else優(yōu)雅得多
六、std::bind——隨心所欲綁定參數(shù)
6.1 為什么要 bind?
function 解決了"統(tǒng)一類型"的問(wèn)題,但還有一個(gè)常見(jiàn)需求沒(méi)解決:我有一組參數(shù),想固定其中幾個(gè),只讓調(diào)用者傳剩下的。
int Sub(int a, int b) {
return (a - b) * 10;
}
假設(shè)我想把 Sub 變成"固定被減數(shù)為 100 的減法函數(shù)",現(xiàn)在調(diào)用者只需要傳減數(shù) b。沒(méi)有 bind 時(shí),你只能再包一層:
// 老辦法:手動(dòng)包裝
auto sub100 = [](int b) {
return Sub(100, b);
};
cout << sub100(5) << endl; // (100 - 5) * 10 = 950
std::bind 讓這個(gè)操作變得異常簡(jiǎn)單:
#include <functional> using namespace std; using placeholders::_1; using placeholders::_2; using placeholders::_3; // 一行搞定:把 Sub 的第一個(gè)參數(shù)固定為 100 auto sub100 = bind(Sub, 100, _1); cout << sub100(5) << endl; // (100 - 5) * 10 = 950
6.2 占位符:_1,_2,_3…
std::bind 的核心是占位符(placeholder)機(jī)制:
_1:代表"綁定后的新函數(shù)的第1個(gè)參數(shù)"_2:代表"綁定后的新函數(shù)的第2個(gè)參數(shù)"_3:代表"綁定后的新函數(shù)的第3個(gè)參數(shù)"
占位符定義在 std::placeholders 命名空間中:
using placeholders::_1; using placeholders::_2; using placeholders::_3; // ... 需要幾個(gè)就 using 幾個(gè)
6.3 bind 的四大經(jīng)典用法
用法一:固定參數(shù)
int Sub(int a, int b) { return (a - b) * 10; }
// 固定第一個(gè)參數(shù)為100
auto sub1 = bind(Sub, 100, _1);
cout << sub1(5) << endl; // Sub(100, 5) = 950
// 固定第二個(gè)參數(shù)為100
auto sub2 = bind(Sub, _1, 100);
cout << sub2(5) << endl; // Sub(5, 100) = -950用法二:調(diào)整參數(shù)順序
// 原本是 Sub(a, b),現(xiàn)在讓調(diào)用者的第一個(gè)參數(shù)傳給b,第二個(gè)傳給a auto sub3 = bind(Sub, _2, _1); cout << sub3(10, 5) << endl; // Sub(5, 10) = -50 (參數(shù)被交換了!) cout << Sub(10, 5) << endl; // Sub(10, 5) = 50 (原始順序)
用法三:多參數(shù)函數(shù)的靈活綁定
int SubX(int a, int b, int c) {
return (a - b - c) * 10;
}
// 分別固定第1、第2、第3個(gè)參數(shù)為100
auto sub5 = bind(SubX, 100, _1, _2); // 固定a=100,b和c由調(diào)用者提供
auto sub6 = bind(SubX, _1, 100, _2); // 固定b=100,a和c由調(diào)用者提供
auto sub7 = bind(SubX, _1, _2, 100); // 固定c=100,a和b由調(diào)用者提供
cout << sub5(5, 1) << endl; // SubX(100, 5, 1) = 940
cout << sub6(5, 1) << endl; // SubX(5, 100, 1) = -960
cout << sub7(5, 1) << endl; // SubX(5, 1, 100) = -960用法四:綁定成員函數(shù)——讓調(diào)用者和普通函數(shù)一樣自然
回憶一下,用 function 包裝成員函數(shù)時(shí),每次調(diào)用都需要顯式傳入對(duì)象。用 bind 可以把對(duì)象也一起綁定進(jìn)去:
class Plus {
public:
Plus(int n = 10) : _n(n) {}
double plusd(double a, double b) {
return (a + b) * _n;
}
private:
int _n;
};
int main() {
// function方式:對(duì)象需要每次傳入
function<double(Plus, double, double)> f6 = &Plus::plusd;
cout << f6(Plus(), 1.1, 1.1) << endl;
// bind方式:對(duì)象一次性綁定,后續(xù)調(diào)用和普通函數(shù)完全一樣
function<double(double, double)> f8 = bind(&Plus::plusd, Plus(), _1, _2);
cout << f8(1.1, 1.1) << endl; // 不需要再傳對(duì)象!
return 0;
}對(duì)比很明顯:
function<double(Plus, double, double)>——三個(gè)參數(shù),每次要傳對(duì)象function<double(double, double)>——兩個(gè)參數(shù),對(duì)象已經(jīng)"內(nèi)置"了
6.4 實(shí)戰(zhàn):用 bind 打造利率計(jì)算工具
最后來(lái)看一個(gè)實(shí)際場(chǎng)景。假設(shè)我們有一個(gè)復(fù)利計(jì)算的核心函數(shù):
// 復(fù)利計(jì)算:返回利息金額
// rate: 年利率,money: 本金,year: 年數(shù)
auto calcInterest = [](double rate, double money, int year) -> double {
double ret = money;
for (int i = 0; i < year; i++) {
ret += ret * rate;
}
return ret - money;
};
cout << calcInterest(0.05, 10000000, 30) << endl; // 5%年利率,1000萬(wàn),30年現(xiàn)在銀行有很多種理財(cái)產(chǎn)品,利率不同、期限不同。直接調(diào)用 calcInterest 每次都要填三個(gè)參數(shù),容易出錯(cuò)。用 bind 可以批量生成不同產(chǎn)品的計(jì)算函數(shù):
// 1.5%年利率系列 function<double(double)> func3_1_5 = bind(calcInterest, 0.015, _1, 3); function<double(double)> func5_1_5 = bind(calcInterest, 0.015, _1, 5); function<double(double)> func10_1_5 = bind(calcInterest, 0.015, _1, 10); // 2.5%年利率系列 function<double(double)> func3_2_5 = bind(calcInterest, 0.025, _1, 3); function<double(double)> func5_2_5 = bind(calcInterest, 0.025, _1, 5); function<double(double)> func10_2_5 = bind(calcInterest, 0.025, _1, 10); // 使用時(shí)只需傳入本金 cout << "1.5% 存3年,100萬(wàn) → " << func3_1_5(1000000) << endl; cout << "1.5% 存5年,100萬(wàn) → " << func5_1_5(1000000) << endl; cout << "1.5% 存10年,100萬(wàn) → " << func10_1_5(1000000) << endl; cout << "2.5% 存3年,100萬(wàn) → " << func3_2_5(1000000) << endl; cout << "2.5% 存5年,100萬(wàn) → " << func5_2_5(1000000) << endl; cout << "2.5% 存10年,100萬(wàn) → " << func10_2_5(1000000) << endl;
用 bind 提前固化利率和年限之后,得到的 function<double(double)> 只接受本金一個(gè)參數(shù)。調(diào)用方不需要關(guān)心利率、年限這些細(xì)節(jié),完全由上下文保證正確性。這在金融系統(tǒng)中非常實(shí)用——可以封裝成一個(gè)個(gè)理財(cái)產(chǎn)品對(duì)象,避免參數(shù)傳遞中的錯(cuò)誤。
七、lambda、function、bind 三者的關(guān)系
學(xué)到這里,你可能會(huì)有個(gè)疑問(wèn):這三個(gè)東西好像能做的事情有重疊?是的,它們之間有明確的定位:
lambda → 快速定義一個(gè)"匿名函數(shù)",就地使用 function → 用一個(gè)"統(tǒng)一的類型"來(lái)保存和傳遞各種可調(diào)用對(duì)象 bind → 把已有的可調(diào)用對(duì)象的某些參數(shù)"固定"住,生成簽名單一的新對(duì)象
一個(gè)典型的配合使用場(chǎng)景:
// 1. 用 lambda 定義核心邏輯
auto calc = [](double rate, double money, int year) -> double {
double ret = money;
for (int i = 0; i < year; i++) ret += ret * rate;
return ret - money;
};
// 2. 用 bind 固定部分參數(shù)
auto fiveYearFund = bind(calc, 0.035, _1, 5);
// 3. 用 function 統(tǒng)一存儲(chǔ)
map<string, function<double(double)>> products;
products["穩(wěn)健型5年"] = fiveYearFund;
products["進(jìn)取型5年"] = bind(calc, 0.06, _1, 5);
// 4. 調(diào)用
cout << products["穩(wěn)健型5年"](100000) << endl;八、總結(jié)
本文從可變參數(shù)模板開(kāi)始,串聯(lián)了 C++11 的一些特性:
可變參數(shù)模板 (Args...)
↓
參數(shù)包展開(kāi)的兩種方式(遞歸展開(kāi) / 逗號(hào)表達(dá)式展開(kāi))
↓
可變參數(shù) + 完美轉(zhuǎn)發(fā) → emplace_back 原地構(gòu)造
↓
lambda 表達(dá)式 → 捕獲列表 / 值 vs 引用 / 隱式捕獲
↓
std::function → 統(tǒng)一各種可調(diào)用對(duì)象的類型
↓
std::bind → 固定參數(shù)、調(diào)整順序、簡(jiǎn)化調(diào)用
這些特性之間是緊密關(guān)聯(lián)的。理解它們之間的"化學(xué)反應(yīng)"比孤立地背語(yǔ)法重要得多:
- 可變參數(shù)模板 + 完美轉(zhuǎn)發(fā) = emplace_back,讓容器的插入性能達(dá)到極致
- lambda + function = 統(tǒng)一的回調(diào)機(jī)制,杜絕了函數(shù)指針、仿函數(shù)各寫(xiě)一套的亂象
- function + bind = 完備的函數(shù)式工具鏈,讓你能像搭積木一樣構(gòu)造復(fù)雜行為
和上一篇文章加起來(lái),C++11 的核心體系你已經(jīng)走完了:統(tǒng)一初始化 → 右值引用 → 移動(dòng)語(yǔ)義 → 萬(wàn)能引用 → 完美轉(zhuǎn)發(fā) → 可變參數(shù)模板 → lambda → function → bind。這套知識(shí)體系是現(xiàn)代 C++ 程序員的"必修課",掌握了它們,你在閱讀和編寫(xiě)現(xiàn)代 C++ 代碼時(shí)會(huì)如魚(yú)得水。
到此這篇關(guān)于C++11:從可變參數(shù)模板到bind綁定器的文章就介紹到這了,更多相關(guān)C++ bind綁定器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Qt實(shí)現(xiàn)TCP客戶端和服務(wù)器通訊程序
這篇文章主要為大家詳細(xì)介紹了Qt實(shí)現(xiàn)TCP客戶端和服務(wù)器通訊程序,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08
php5系列的apache遠(yuǎn)程執(zhí)行漏洞攻擊腳本
這篇文章主要介紹了php5系列的apache遠(yuǎn)程執(zhí)行漏洞攻擊腳本,需要的朋友可以參考下2014-06-06
vscode 配置 C/C++ 編譯環(huán)境的詳細(xì)圖文教程
這篇文章主要介紹了vscode 配置 C/C++ 編譯環(huán)境的詳細(xì)教程,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-05-05
C語(yǔ)言基礎(chǔ)使用IDE快速開(kāi)發(fā)的方法
這篇文章主要介紹了C語(yǔ)言基礎(chǔ)使用IDE快速開(kāi)發(fā)的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11
c++函數(shù)指針和回調(diào)函數(shù)示例
這篇文章主要介紹了c++函數(shù)指針和回調(diào)函數(shù)示例,需要的朋友可以參考下2014-05-05
C++ VTK實(shí)例之高斯隨機(jī)數(shù)的生成
這篇文章主要介紹了VTK的一個(gè)實(shí)例之高斯隨機(jī)數(shù)的生成,本文演示了從一個(gè)平均數(shù)是0.0和標(biāo)準(zhǔn)偏差是2.2的高斯分布中隨機(jī)生成3個(gè)隨機(jī)數(shù)。感興趣的同學(xué)可以學(xué)習(xí)一下2021-11-11

