利用 C++ 的 Lambda 表示式提升 Qt 程式碼
Lambda 表示式是在 C++11 中加入的 C++ 特性。在這篇文章中我們將看到如何用 Lambda 表示式來簡化 Qt 程式碼。Lambda 很強大,但也要小心它帶來的陷阱。
首先,什麼是 Labmda 表示式?
Lambda 表示式是在某個函式中直接定義的匿名函式。它可以用於任何需要傳遞函式指標的地方。
Lambda 表示式的語法如下:
1 2 3 |
[獲取變數](引數) { lambda 程式碼 } |
現在先忽略 “獲取變數” 這部分。下面是一個簡單的 Lambda,用於遞增一個數:
1 2 3 |
[](int value) { return value + 1; } |
我們可以把這個 Lambda 用於像 std::transform() 這樣的函式,來為 vector 的每一個元素增值:
1 2 3 4 5 6 7 8 |
#include #include #include int main() { std::vector vect = { 1, 2, 3 }; std::transform(vect.begin(), vect.end(), vect.begin(), [](int value) { return value + 1; }); for(int value : vect) { std::cout |
列印結果:
1 2 3 |
2 3 4 |
獲取變數
Lambda 表示式可以通過 “獲取” 來使用當前作用域中的變數。下面是用 Lambda 來對 vector 求和的一個示例。
1 2 3 4 5 |
std::vector vect = { 1, 2, 3 }; int sum = 0; std::for_each(vect.begin(), vect.end(), [&sum](int value) { sum += value; }); |
你可以看到,我們獲取了本地變數 sum,所以可以在 Lambda 內部使用它。sum 加了字首 &,這表示我們通過引用獲取 sum 變數:在 Lambda 內部,sum 是一個引用,所以對它進行的任何改變都會對 Lambda 外部的 sum 變數造成影響。
如果你不是需要引用,只需要變數的拷貝,只需要去掉 & 就好。
如果你想獲取多個變數,只需要用逗號進行分隔,就像函式的引數那樣。
目前還不能直接獲取成員變數,但是你可以獲取 this,然後通過它訪問當前物件的所有成員。
在背後,Lambda 獲取的變數會儲存在一個隱藏的物件中。不過,如果編譯器確認 Lambda 不會在當前區域性作用域之外使用,它就會進行優化,直接使用局域變數。
有一個偷懶的辦法可以獲取所有區域性變數。用 [&] 來獲取它們的引用;用 [=] 來獲取它們的拷貝。不過最好不要這樣做,因為引用變更的生命週期很可能短於 Lambda 的生命週期,這會導致奇怪的錯誤。就算你獲取的是一個變數的拷貝,但它本身是一個指標,也會導致崩潰。如果明確的列出你依賴的變數,會更容易避開這類陷阱。關於這個陷阱更多的資訊,請看看 “Effective Modern C++” 的第 31 條。
Qt 連線中的 Lambda
如果你在用新的連線風格 (你應該用,因為有非常好的型別安全!),就可以在接收端使用 Lambda,這對於較小的處理函式來說簡直太棒了。
下面是一個電話括號器的示例,使用者可以輸入數字然後撥出電話:
1 2 3 4 5 6 7 8 9 10 |
Dialer::Dialer() { mPhoneNumberLineEdit = new QLineEdit(); QPushButton* button = new QPushButton("Call"); /* ... */ connect(button, &QPushButton::clicked, this, &Dialer::startCall); } void Dialer::startCall() { mPhoneService->call(mPhoneNumberLineEdit->text()); } |
我們可以使用 Lambda 代替 startCall() 方法:
1 2 3 4 5 6 7 8 |
Dialer::Dialer() { mPhoneNumberLineEdit = new QLineEdit(); QPushButton* button = new QPushButton("Call"); /* ... */ connect(button, &QPushButton::clicked, [this]() { mPhoneService->call(mPhoneNumberLineEdit->text()); }); } |
用 Lambda 代替 QObject::sender()
Lambda 也是 QObject::sender() 的一個非常好的替代方案。想像一下,如果我們的撥號器現在是一組的數字按鈕的陣列。
沒使用 Labmda 的程式碼,在組合數字的時候會像這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Dialer::Dialer() { for (int digit = 0; digit setProperty("digit", digit); connect(button, &QPushButton::clicked, this, &Dialer::onClicked); } /* ... */ } void Dialer::onClicked() { QPushButton* button = static_cast(sender()); int digit = button->property("digit").toInt(); mPhoneService->dial(digit); } |
我們可以使用 QSignalMapper 並去掉 Dialer::onClicked() 方法,但使用 Labmda 會更靈活更簡單。我們只需要獲取與按鈕對應的數字,然後在 Lambda 中直接就能呼叫 mPhoneService->dial()。
1 2 3 4 5 6 7 |
Dialer::Dialer() { for (int digit = 0; digit dial(digit); } ); } /* ... */ } |
不要忘了物件的生命週期!
看這段程式碼:
1 2 3 4 |
void Worker::setMonitor(Monitor* monitor) { connect(this, &Worker::progress, monitor, &Monitor::setProgress); } |
在這個小例子中,有一個 Worker 例項來向 Monitor 例項報告進度。到目前為止,還沒什麼問題。
現在假設 Worker::progress() 有一個 int 型的引數,並且 monitor 的另一個方法需要使用這個引數值。我們會嘗試這樣做:
1 2 3 4 5 6 7 8 9 |
void Worker::setMonitor(Monitor* monitor) { // Don't do this! connect(this, &Worker::progress, [monitor](int value) { if (value setProgress(value); } else { monitor->markFinished(); } }); } |
看起來沒問題……但是這段程式碼會導致崩潰!
Qt 的連線系統很智慧,如果傳送方和接收方中的任何一個被刪除掉,它就會刪除連線。在最初的 setMonitor() 中,如果 monitor 被刪除了,連線也會被刪除。但現在我們使用了 Lambda 來作為接收方: Qt 目前沒有辦法發現在 Lambda 中使用了 monitor。即使 monitor 被刪除掉,Lambda 仍然會呼叫,結果應用就會在嘗試引用 monitor 的時候發生崩潰。
為了避免崩潰發生,你要向 connect() 呼叫傳入一個“context”引數,像這樣:
1 2 3 4 5 6 7 8 9 10 |
void Worker::setMonitor(Monitor* monitor) { // Do this instead! connect(this, &Worker::progress, monitor, [monitor](int value) { if (value setProgress(value); } else { monitor->markFinished(); } }); } |
這段程式碼中,我們把 monitor 作為上下文傳入了 connect()。這不會對 Lambda 的執行造成影響,但是在 monitor 被刪除之後,Qt 會注意到並解除 Worker::progress() 和 Lambda 之間的連線。
這個上下文還會用於檢測連線是否在佇列中。就像經典的 signal-slot 連線那樣,如果上下文物件與發射訊號的程式碼不在同一個執行緒,Qt 會將連線置入佇列。
代替 QMetaObject::invokeMethod
1 2 3 4 |
class Foo : public QObject { public slots: void doSomething(int x); }; |
你可以在 Qt 中使用 QMetaObject::invokeMethod 在事件迴圈返回時呼叫 Foo::doSomething():
1 2 |
QMetaObject::invokeMethod(this, "doSomething", Qt::QueuedConnection, Q_ARG(int, 1)); |
這段程式碼會工作,但是:
- 語法太醜
- 非型別安全
- 你必須定義作為 slot 的方法
1 2 3 |
QTimer::singleShot(0, [this]() { doSomething(1); }); |
這個效率會稍低一些,因為 QTimer::singleShot() 會在背後建立一個物件,不過,只要你不是要在一秒內呼叫很多次,這點效能損失可以忽略不計。顯然利大於弊。
你同樣可以在 Lambda 前面指定一個上下文,這在多執行緒中非常有用。但要小心:如果你使用低於 5.6.0 版本的 Qt,QTimer::singleShot() 有一個 BUG 在多執行緒中使用時會導致崩潰。我們找到了那個困難的辦法……
關鍵點
- 連線 Qt 物件的時候使用 Lambda 比使用排程方法更好
- 在 connect() 呼叫中使用 Lambda 一定要有上下文
- 按需獲取變數
希望你能喜歡這篇文章,並希望你現在就用漂亮的 Lambda 語法替換掉古板的舊語法!