如何利用 C++ 的 Lambda 表示式提升 Qt 程式碼

發表於2017-02-09

Lambda 表示式是在 C++11 中加入的 C++ 特性。在這篇文章中我們將看到如何用 Lambda 表示式來簡化 Qt 程式碼。Lambda 很強大,但也要小心它帶來的陷阱。

首先,什麼是 Labmda 表示式?

Lambda 表示式是在某個函式中直接定義的匿名函式。它可以用於任何需要傳遞函式指標的地方。

Lambda 表示式的語法如下:

現在先忽略 “獲取變數” 這部分。下面是一個簡單的 Lambda,用於遞增一個數:

我們可以把這個 Lambda 用於像 std::transform() 這樣的函式,來為 vector 的每一個元素增值:

列印結果:

獲取變數

Lambda 表示式可以通過 “獲取” 來使用當前作用域中的變數。下面是用 Lambda 來對 vector 求和的一個示例。

你可以看到,我們獲取了本地變數 sum,所以可以在 Lambda 內部使用它。sum 加了字首 &,這表示我們通過引用獲取 sum 變數:在 Lambda 內部,sum 是一個引用,所以對它進行的任何改變都會對 Lambda 外部的 sum 變數造成影響。

如果你不是需要引用,只需要變數的拷貝,只需要去掉 & 就好。

如果你想獲取多個變數,只需要用逗號進行分隔,就像函式的引數那樣。

目前還不能直接獲取成員變數,但是你可以獲取 this,然後通過它訪問當前物件的所有成員。

在背後,Lambda 獲取的變數會儲存在一個隱藏的物件中。不過,如果編譯器確認 Lambda 不會在當前區域性作用域之外使用,它就會進行優化,直接使用局域變數。

有一個偷懶的辦法可以獲取所有區域性變數。用 [&] 來獲取它們的引用;用 [=] 來獲取它們的拷貝。不過最好不要這樣做,因為引用變更的生命週期很可能短於 Lambda 的生命週期,這會導致奇怪的錯誤。就算你獲取的是一個變數的拷貝,但它本身是一個指標,也會導致崩潰。如果明確的列出你依賴的變數,會更容易避開這類陷阱。關於這個陷阱更多的資訊,請看看 “Effective Modern C++” 的第 31 條。

Qt 連線中的 Lambda

如果你在用新的連線風格 (你應該用,因為有非常好的型別安全!),就可以在接收端使用 Lambda,這對於較小的處理函式來說簡直太棒了。

下面是一個電話括號器的示例,使用者可以輸入數字然後撥出電話:

我們可以使用 Lambda 代替 startCall() 方法:

用 Lambda 代替 QObject::sender()

Lambda 也是 QObject::sender() 的一個非常好的替代方案。想像一下,如果我們的撥號器現在是一組的數字按鈕的陣列。

沒使用 Labmda 的程式碼,在組合數字的時候會像這樣:

我們可以使用 QSignalMapper 並去掉 Dialer::onClicked() 方法,但使用 Labmda 會更靈活更簡單。我們只需要獲取與按鈕對應的數字,然後在 Lambda 中直接就能呼叫 mPhoneService->dial()。

不要忘了物件的生命週期!

看這段程式碼:

在這個小例子中,有一個 Worker 例項來向 Monitor 例項報告進度。到目前為止,還沒什麼問題。

現在假設 Worker::progress() 有一個 int 型的引數,並且 monitor 的另一個方法需要使用這個引數值。我們會嘗試這樣做:

看起來沒問題……但是這段程式碼會導致崩潰!

Qt 的連線系統很智慧,如果傳送方和接收方中的任何一個被刪除掉,它就會刪除連線。在最初的 setMonitor() 中,如果 monitor 被刪除了,連線也會被刪除。但現在我們使用了 Lambda 來作為接收方: Qt 目前沒有辦法發現在 Lambda 中使用了 monitor。即使 monitor 被刪除掉,Lambda 仍然會呼叫,結果應用就會在嘗試引用 monitor 的時候發生崩潰。

為了避免崩潰發生,你要向 connect() 呼叫傳入一個“context”引數,像這樣:

這段程式碼中,我們把 monitor 作為上下文傳入了 connect()。這不會對 Lambda 的執行造成影響,但是在 monitor 被刪除之後,Qt 會注意到並解除 Worker::progress() 和 Lambda 之間的連線。

這個上下文還會用於檢測連線是否在佇列中。就像經典的 signal-slot 連線那樣,如果上下文物件與發射訊號的程式碼不在同一個執行緒,Qt 會將連線置入佇列。

代替 QMetaObject::invokeMethod

你可能對一種非同步呼叫 slot 的方法比較熟悉,它使用 QMetaObject::invokeMethod。先定義一個類:

你可以在 Qt 中使用 QMetaObject::invokeMethod 在事件迴圈返回時呼叫 Foo::doSomething():

這段程式碼會工作,但是:

  • 語法太醜
  • 非型別安全
  • 你必須定義作為 slot 的方法

可以通過在 QTimer::singleShot() 中呼叫 Lambda 來代替上面的程式碼:

這個效率會稍低一些,因為  QTimer::singleShot() 會在背後建立一個物件,不過,只要你不是要在一秒內呼叫很多次,這點效能損失可以忽略不計。顯然利大於弊。

你同樣可以在 Lambda 前面指定一個上下文,這在多執行緒中非常有用。但要小心:如果你使用低於 5.6.0 版本的 Qt,QTimer::singleShot() 有一個 BUG 在多執行緒中使用時會導致崩潰。我們找到了那個困難的辦法……

關鍵點

  • 連線 Qt 物件的時候使用 Lambda 比使用排程方法更好
  • 在 connect() 呼叫中使用 Lambda 一定要有上下文
  • 按需獲取變數

希望你能喜歡這篇文章,並希望你現在就用漂亮的 Lambda 語法替換掉古板的舊語法!

相關文章