Item 34:優先使用 lambda 而非 std::bind (Prefer Lambdas to std::bind)
Overview Table
| 比較面向 | Lambda | std::bind |
|---|---|---|
| 可讀性 | 呼叫就是一般函式呼叫,一目了然 | placeholder(_1、_2)如同魔法,需心算對應位置 |
| 引數求值時機 | 寫在 lambda body 內 → 呼叫時求值 | 直接傳給 std::bind → bind 時立即求值,需巢狀 std::bind 才能延遲 |
| 重載函式 | overload resolution 正常運作 | 只拿到函式名稱 → 歧義,需 static_cast 成函式指標 |
| inline 可能性 | 直接呼叫,容易被 inline | 透過函式指標呼叫,較難 inline → 可能較慢 |
| 儲存方式(captured/bound 物件) | capture 子句明白寫出 by value / by reference | 一律以值儲存,程式碼上看不出來 |
| 呼叫時引數傳遞 | 參數列明白寫出 | 一律以參考傳遞(perfect forwarding),必須背規則 |
| 結論 | C++14 起一律用 lambda | C++11 僅剩兩個受限用例(見下) |
可讀性與引數求值時機:setAlarm 範例
using Time = std::chrono::steady_clock::time_point;
enum class Sound { Beep, Siren, Whistle };
using Duration = std::chrono::steady_clock::duration;
void setAlarm(Time t, Sound s, Duration d); // 在時刻 t 發出聲音 s 持續 d
// Lambda 版本:呼叫 setAlarm 是普通函式呼叫,時間運算式寫在 body 內
auto setSoundL = [](Sound s) {
using namespace std::chrono;
using namespace std::literals; // C++14 時間字面值後綴
setAlarmnow( + 1h, // 「呼叫 setSoundL 時」才求值
s,
30s);
};
std::bind 的初版寫法有 隱性 bug:
using namespace std::chrono;
using namespace std::literals;
using namespace std::placeholders; // 使用 _1 必須引入
auto setSoundB = std::bind(setAlarm,
steady_clock::now() + 1h, // 錯!bind 當下就求值
_1,
30s);
引數求值時機(關鍵差異):
Lambda: 定義 setSoundL ──────────► 呼叫 setSoundL(s)
│
└─ now() + 1h 此刻才計算 ✓ 正確
std::bind: 呼叫 std::bind(...) ────────► 呼叫 setSoundB(s)
│ │
└─ now() + 1h 立刻計算、 └─ 使用「舊的」時間值
存入 bind object ✗ 鬧鐘提早響
修正需要巢狀 std::bind 來延遲求值,可讀性雪上加霜:
auto setSoundB =
std::bind(setAlarm,
std::bindplus<>(), steady_clock::now(), 1h, // C++14
_1, // C++11 須寫
30s); // std::plus<time_point>
重載歧義與 inline 效率
當 setAlarm 增加重載(如第四個參數 Volume):
void setAlarm(Time t, Sound s, Duration d, Volume v); // 新重載
// Lambda:body 內是一般呼叫,overload resolution 自動選出 3 參數版本 ✓
// std::bind:只有函式「名稱」,編譯器無法判斷要綁哪一個 → 編譯錯誤 ✗
using SetAlarm3ParamType = void(*)(Time, Sound, Duration);
auto setSoundB =
std::bind(static_cast<SetAlarm3ParamType>(setAlarm), // 必須手動 cast
std::bindplus<>(), steady_clock::now(), 1h,
_1, 30s);
| 效率影響 | 說明 |
|---|---|
setSoundLSiren |
closure 的 operator() 內是普通函式呼叫 → 容易 inline |
setSoundBSiren |
bind object 內部持有函式指標 → 編譯器較難 inline,lambda 可能產生更快的程式碼 |
再看區間判斷範例——lambda 一行看懂,std::bind 是「以晦澀換飯碗」:
auto betweenL = [lowVal, highVal](const auto& val) // C++14
{ return lowVal <= val && val <= highVal; };
auto betweenB =
std::bindlogical_and<>(, // 需三層巢狀 bind
std::bindless_equal<>(), lowVal, _1,
std::bindless_equal<>(), _1, highVal);
儲存與傳遞方式的不透明性
Widget w;
using namespace std::placeholders;
auto compressRateB = std::bind(compress, w, _1); // w 以值?以參考?看不出來
auto compressRateL = [w](CompLevel lev) // 明白寫出:w 以值 capture
{ return compress(w, lev); };
compressRateBHigh; // 引數如何傳入?必須背:一律以參考(perfect forwarding)
compressRateLHigh; // 參數列寫明:lev 以值傳遞
- bind object 儲存:
std::bind對 lvalue 引數 copy construct、對 rvalue 引數 move construct,一律「以值」存入 bind object,呼叫端程式碼毫無線索。 - bind object 呼叫:所有傳給 bind object 的引數都以參考傳給底層 callable(其
operator()用 perfect forwarding)。
「一律以值儲存」的例外
對引數套用 std::ref 可得到「以參考儲存」的效果:std::bind(compress, std::ref(w), _1) 之後,compressRateB 的行為就像持有 w 的參考,w 的後續修改會被反映。
C++11 僅存的兩個 std::bind 用例
| 用例 | 原因 | C++14 取代方案 |
|---|---|---|
| Move capture 模擬 | C++11 lambda 無法把物件 move 進 closure,但可 move 進 bind object,再以參考傳給 lambda | init capture([x = std::move(x)],見 Item 32) |
| 綁定多型函式物件 (polymorphic function objects) | bind object 的 operator() 用 perfect forwarding,可把不同型別引數轉給模板化 operator() |
auto 參數的 generic lambda |
class PolyWidget {
public:
template<typename T>
void operator()(const T& param); // 模板化的函式呼叫運算子
};
PolyWidget pw;
auto boundPW = std::bind(pw, _1); // C++11:只能靠 std::bind
boundPW(1930); // 傳 int
boundPW(nullptr); // 傳 nullptr
boundPW("Rosebud"); // 傳字串字面值
auto boundPW14 = [pw](const auto& param) // C++14:generic lambda 輕鬆取代
{ pw(param); };
例外的例外
- bind object 的 perfect forwarding 仍受 完美轉發失敗案例(braced initializer、bitfield 等,見 Item 30)限制。
- 這兩個用例只在 C++11 成立;C++14 起沒有任何合理的
std::bind使用情境——init capture 與 auto 參數已完全消除其必要性。
Things to Remember
- Lambda 比
std::bind更可讀、更有表達力,也可能更有效率(inline 機會較高)。 - 僅在 C++11,
std::bind對「move capture 模擬」與「綁定含模板化operator()的物件」兩種情境可能有用;C++14 起一律改用 lambda。
Exam/Test Patterns
| 情境關鍵字 | 答案 |
|---|---|
std::bind(setAlarm, now() + 1h, _1, 30s) 鬧鐘時間錯誤 |
運算式在呼叫 std::bind 時求值並存入 bind object;延遲求值需巢狀 std::bindplus<>(), ... |
| bind 重載函式編譯失敗 | 函式名稱有歧義;需 static_cast 成特定函式指標型別;lambda 則由 overload resolution 自動解決 |
| lambda vs bind 誰可能較快 | lambda——body 內為直接呼叫可 inline;bind 透過函式指標呼叫難 inline |
| bind object 如何儲存引數 | 一律以值(lvalue → copy、rvalue → move);要參考語意須用 std::ref |
| 呼叫 bind object 時引數如何傳遞 | 一律以參考(operator() 使用 perfect forwarding) |
C++11 何時 std::bind 仍合理 |
兩例:模擬 move capture、綁定 polymorphic(模板化 operator())函式物件 |
C++14 該用 std::bind 嗎 |
不該;init capture + auto 參數 generic lambda 已涵蓋所有情境 |
placeholder _1 的意義 |
「呼叫 bind object 時的第 1 個引數」對應到被綁函式的該參數位置;需 using namespace std::placeholders |
Related Notes
- 07-Lambda-Expressions/02-Init-Capture — C++14 init capture 取代
std::bind的 move capture 模擬(Item 32) - 07-Lambda-Expressions/01-Avoid-Default-Capture-Modes — lambda capture 明確性的重要性,與本 Item 的「顯式優於隱式」精神一致
- 07-Lambda-Expressions/03-Decltype-Auto-Forwarding — generic lambda 的 auto&& 參數與轉發,是取代 polymorphic bind object 的基礎
- 06-Move-Semantics-And-Perfect-Forwarding/08-Perfect-Forwarding-Failure-Cases — bind object 的 perfect forwarding 同樣受這些失敗案例限制(Item 30)
- 06-Move-Semantics-And-Perfect-Forwarding/01-Std-Move-And-Std-Forward —
std::move使 rvalue 引數被 move construct 進 bind object(Item 23) - 01-Introduction/01-Terms-And-Conventions — function object、closure、callable 等術語定義