(原文連結:https://abseil.io/tips/108 譯者:[email protected])
每周貼士 #108: 避免使用 std::bind
std::bind
- 最初釋出于2016-01-07
- 作者:Roman Perepelitsa ([email protected])
- (譯者注:這哥們兒是C++大神,有興趣可以上網搜搜他的文章和代碼)
- (譯者注:現在離開Google去做舉重運動員了,大寫的佩服!)
- 更新于2019-12-19
- 短連結:abseil.io/tips/108
避免使用 std::bind
std::bind
這條貼士總結了為什麼你寫代碼應該遠離
std::bind()
的原因。
正确使用
std::bind()
太難了。一起來看幾個例子。這段代碼看起來行不?
void DoStuffAsync(std::function<void(Status)> cb);
class MyClass {
void Start() {
DoStuffAsync(std::bind(&MyClass::OnDone, this));
}
void OnDone(Status status);
};
很多C++老油條工程師們寫過類似的代碼,然後發現編譯不過。
std::function<void()>
(譯者注:函數簽名裡沒有參數)用起來好好的,但是給
MyClass::OnDone
加個參數就跪了。什麼情況?
std::bind()
不隻是綁定靠前的N個參數,這與很多C++工程師預期的行為(偏函數)(譯者注:維基百科打不開的話,我沒找到C++版本的中文解釋,這個JavaScript的也可以湊合看,知道意思就行)不一緻。你必須指定所有參數,是以催動
std::bind()
正确的咒語是:
那個啥,真醜。有木有好點兒的方式?還真有,用
absl::bind_front()
。
還記得早前提到
std::bind()
沒實作的偏函數嗎?
absl::bind_front()
精準實作了這個功能:它綁定靠前的N個參數,然後完美轉發剩下的參數:
absl::bind_front(F, a, b)(x, y)
展開成
F(a, b, x, y)
。
你看,世界又科學了。想來點兒刺激的不?下面的代碼是什麼行為?
void DoStuffAsync(std::function<void(Status)> cb);
class MyClass {
void Start() {
DoStuffAsync(std::bind(&MyClass::OnDone, this));
}
void OnDone(); // 沒有Status參數.
};
OnDone()
不接受參數,傳給
DoStuffAsync()
的回調函數應該接受一個
Status
參數。你也許會預期編譯錯誤,但實際上編譯會成功,而且連條警告都沒有,因為
std::bind
過于激進地彌合了兩者的不一緻(譯者注:收不收
Status
參數)。
DoStuffAsync()
裡可能出現的錯誤(譯者注:表現為
Status
對象,在傳給回調函數時)被悄悄地忽略了。
這樣的代碼有可能帶來嚴重傷害。比如一個輸入輸出操作跪了,但是調用端以為它成功了,那酸爽可能是毀滅性的。也許
MyClass
的作者根本沒意識到
DoStuffAsync()
有可能出現一個本應被處理的錯誤。或者
DoStuffAsync()
以前接收
std::function<void()>
參數,但後來作者決定引入錯誤狀态,然後手動更新所有編譯報錯的調用端代碼。不管是哪種情況,bug就這樣溜進了生産環境的代碼。
std::bind()
癱瘓了我們強烈依賴的編譯期檢查。如果調用端給你的函數傳了多餘的參數,通常編譯器會告訴你一聲,但
std::bind()
讓編譯器啞火了。你以為這就夠刺激了?
再來個例子。你認為這段代碼怎麼樣?
void Process(std::unique_ptr<Request> req);
void ProcessAsync(std::unique_ptr<Request> req) {
thread::DefaultQueue()->Add(
ToCallback(std::bind(&MyClass::Process, this, std::move(req))));
}
跨作用域傳遞
std::unique_ptr
的經典方式。甭問,
std::bind()
肯定不靈——這段代碼編譯不過,因為
std::bind()
不支援把隻能移動(move-only)(譯者注:不能複制)的參數傳遞給目标函數。把
std::bind()
替換為
absl::bind_front()
就行了。
下一個例子,就算是C++專家,也通常會絆一跟頭。看看你能不能發現其中的問題。
// F必須是接收0個參數的可調用對象。
template <class F>
void DoStuffAsync(F cb) {
auto DoStuffAndNotify = [](F cb) {
DoStuff();
cb();
};
thread::DefaultQueue()->Schedule(std::bind(DoStuffAndNotify, cb));
}
class MyClass {
void Start() {
DoStuffAsync(std::bind(&yClass::OnDone, this));
}
void OnDone();
};
這段代碼編譯不過,因為把
std::bind()
的結果傳遞給另一個
std::bind()
是個特殊情況。通常情況下,
std::bind(F, arg)()
展開成
F(arg)
。但如果
arg
是另一個
std::bind()
的結果時,它展開成
F(arg())
。如果先把
arg
轉化為
std::function<void()>
,這個神奇的特殊行為就沒了。
将
std::bind()
用于不歸你控制的類型是一個bug。
DoStuffAsync()
不該将
std::bind()
用于模闆參數上。改用
absl::bind_front()
或lambda就行了。
DoStuffAsync()
的作者甚至有可能看到測試一路綠燈,因為單元測試裡永遠會丢給它lambda或
std::function
做參數,但永遠不會把
std::bind()
的結果丢給它。
MyClass
的作者撞上這個bug的時候肯定一臉懵。
退一萬步講,
std::bind()
的特殊行為有用嗎?毛用沒有。它就是塊絆腳石。如果你正試圖通過嵌套調用
std::bind()
來組合函數,你真的應該寫個lambda或者普通函數。
希望你已經接受了“
std::bind()
容易被用錯”這個觀點。運作期和編譯器的陷阱既坑新手又坑C++專家。現在我想展示給你:就算
std::bind()
被用對了,通常也會有可讀性更高的替代方案。
不用占位符(placeholder)的
std::bind()
不如改用lambda。
對比
用
std::bind()
實作的偏函數不如改用
absl::bind_front()
。占位符越多,差距越明顯。
對比
(實作偏函數的時候,用
absl::bind_front()
還是用lambda可以看着辦,自己決定。)
這裡覆寫了99%的
std::bind()
調用場景。剩下的場景就比較秀了:
- 忽略掉部分參數:
。std::bind(F, _2)
- 同一個參數用多次:
。std::bind(F, _1, _1)
- 綁定靠後的參數:
。std::bind(F, _1, 42)
- 改變參數順序:
。std::bind(F, _2, _1)
- 組合函數:
。std::bind(F, std::bind(G))
- 以上任意一種,外加結果要求為多态函數對象。
這些進階應用也許有其用武之地。在決定使用它們之前,先想想已知的
std::bind()
的坑,然後問問你自己,省下的幾個字元或幾行代碼是不是值得付出這麼大代價。
結論
避開
std::bind
。改用lambda或
absl::bind_front
。
延伸閱讀
'’Effective Modern C++’’, Item 34: Prefer lambdas to std::bind.