Item 34:優先使用 lambda 而非 std::bind (Prefer Lambdas to std::bind)

Overview Table

比較面向 Lambda std::bind
可讀性 呼叫就是一般函式呼叫,一目了然 placeholder(_1_2)如同魔法,需心算對應位置
引數求值時機 寫在 lambda body 內 → 呼叫時求值 直接傳給 std::bindbind 時立即求值,需巢狀 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 以值傳遞
「一律以值儲存」的例外

對引數套用 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); };
例外的例外

  1. bind object 的 perfect forwarding 仍受 完美轉發失敗案例(braced initializer、bitfield 等,見 Item 30)限制。
  2. 這兩個用例只在 C++11 成立;C++14 起沒有任何合理的 std::bind 使用情境——init capture 與 auto 參數已完全消除其必要性。

Things to Remember

  • Lambda 比 std::bind 更可讀、更有表達力,也可能更有效率(inline 機會較高)。
  • 僅在 C++11std::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