天天看點

Google C++每周貼士 #108: 避免使用std::bind每周貼士 #108: 避免使用std::bind

(原文連結:https://abseil.io/tips/108 譯者:[email protected])

每周貼士 #108: 避免使用

std::bind

  • 最初釋出于2016-01-07
  • 作者:Roman Perepelitsa ([email protected])
    • (譯者注:這哥們兒是C++大神,有興趣可以上網搜搜他的文章和代碼)
    • (譯者注:現在離開Google去做舉重運動員了,大寫的佩服!)
  • 更新于2019-12-19
  • 短連結:abseil.io/tips/108

避免使用

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.

繼續閱讀