1. 程式人生 > >利用 C++ 的 Lambda 表示式提升 Qt 程式碼

利用 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 語法替換掉古板的舊語法!