Lambda 表达式提升 Qt 代码

Lambda 表达式是在 C++11 中加入的 C++ 特性。在这篇文章中我们将看到如何用 Lambda 表达式来简化 Qt 代码。Lambda 很强大,但也要小心它带来的陷阱。

首先,什么是 Labmda 表达式?

简单来说,Lambda函数也就是一个函数。在Qt中信号的槽函数可以使用Lamba表达式来代替,作为槽函数的替代函数。Lambda是C++11添加的内容,在Qt5中使用需要在项目文件中添加 CONFIG += c++11 

Lambda的基本类型为: [ ] ( ) { }

与普通函数void change() { }相比,只有括号和大括号相同,作用也是相同的,即:括号()用来传递参数,大括号{}内为函数体

方括号 [ ] 用来传递局部变量,可在 [ ] 内枚举函数体内需要用到的局部变量。使用 [ = ] 则外部的所有变量函数体内都可使用。同理 [ & ] 用引用的方法也相同,但是如果需要引用的变量是动态的,例如指针,则这种方法可能会引发出错,内存释放不及时,导致其他变量的值出错。 [ this ] 这也类似,都能使用函数体外部的变量。

括号()用来传递参数,如果对应的信号函数有参数,例如:clicked (bool )信号函数,则可在括号内添加相应类型的参数,方便传参。

但是默认在函数体内不允许修改外面的变量的值,如果需要修改,则需要在括号后面添加关键字 mutable 。即: [ = ] (bool chan)mutable { } 

示例代码:

QPushButton *b4 = new QPushButton(this);  //创建新按钮
b4->setText("Lambda表达式");              //修改按钮名字
b4->move(150, 150);                       //按钮位置
int a = 10, b=20;
connect(b4, &QPushButton::released,       //连接函数,使用Lambda函数则不必指定接收对象,只需利用Lambda函数来代替槽函数即可
        [=]() mutable
        {
            b4->setText("change");
            a = 11;
            qDebug()<<a << b;
            qDebug() << "Lambda表达式";  //打印
        }
        );   //括号不要落下

一、它的语法定义如下:

它的语法定义:[capture] (parameters) mutable -> return-type {statement}

各个参数说明:

1.[capture]:捕捉列表。捕捉列表总是出现在Lambda函数的开始处。实际上,[]是Lambda引出符。编译器根据该引出符判断接下来的代码是否是Lambda函数。捕捉列表能够捕捉上下文中的变量以供Lambda函数使用;

2.(parameters):参数列表。与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号“()”一起省略;

3.mutable:mutable修饰符。默认情况下,Lambda函数总是一个const函数,mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空);   (这里要记住,不然你在Connect里面想赋值给外面的变量时,会报错 例如会报 :C2678: 二进制“=”: 没有找到接受“const QString”类型的左操作数的运算符 , 这个错误是我在没有加 mutable 修饰符的时候,在外面定义了一个变量 (QString abc;) 然后在 connect 里面定义一个 QString def; 然后直接  abc = def; 就报错,因为在connect内部 def 的类型是const 类型的。)

4.->return-type:返回类型。用追踪返回类型形式声明函数的返回类型。我们可以在不需要返回值的时候也可以连同符号”->”一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导;

5.{statement}:函数体。内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。

二、与普通函数最大的区别是,除了可以使用参数以外,Lambda函数还可以通过捕获列表访问一些上下文中的数据。

具体地,捕捉列表描述了上下文中哪些数据可以被Lambda使用,以及使用方式(以值传递的方式或引用传递的方式)。语法上,在“[]”包括起来的是捕捉列表,捕捉列表由多个捕捉项组成,并以逗号分隔。捕捉列表有以下几种形式:

1.[var]表示值传递方式捕捉变量var;
2.[=]表示值传递方式捕捉所有父作用域的变量(包括this);
3.[&var]表示引用传递捕捉变量var;
4.[&]表示引用传递方式捕捉所有父作用域的变量(包括this);
5.[this]表示值传递方式捕捉当前的this指针。

上面提到了一个父作用域,也就是包含Lambda函数的语句块,说通俗点就是包含Lambda的“{}”代码块。上面的捕捉列表还可以进行组合,例如:

1.[=,&a,&b]表示以引用传递的方式捕捉变量a和b,以值传递方式捕捉其它所有变量;
2.[&,a,this]表示以值传递的方式捕捉变量a和this,引用传递方式捕捉其它所有变量。

不过值得注意的是,捕捉列表不允许变量重复传递。下面一些例子就是典型的重复,会导致编译时期的错误。例如:

3.[=,a]这里已经以值传递方式捕捉了所有变量,但是重复捕捉a了,会报错的;
4.[&,&this]这里&已经以引用传递方式捕捉了所有变量,再捕捉this也是一种重复。

 

Lambda 表达式是在某个函数中直接定义的匿名函数。它可以用于任何需要传递函数指针的地方。

Lambda 表达式的语法如下:

[获取变量](参数) {
    lambda 代码
}

现在先忽略 “获取变量” 这部分。下面是一个简单的 Lambda,用于递增一个数:

[](int value) {
    return value + 1;
}

我们可以把这个 Lambda 用于像 std::transform() 这样的函数,来为 vector 的每一个元素增值:

#include <algorithm>
#include <iostream>
#include <vector>
int main() {
    std::vector<int> vect = { 1, 2, 3 };
    std::transform(vect.begin(), vect.end(), vect.begin(),
    [](int value) { return value + 1;});

    for(int value : vect)
    {
        std::cout << value << std::endl;
    }
    return 0;
}

打印结果 :

2
3
4

获取变量

Lambda 表达式可以通过 “获取” 来使用当前作用域中的变量。下面是用 Lambda 来对 vector 求和的一个示例。

std::vector<int> 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,这对于较小的处理函数来说简直太棒了。

下面是一个电话括号器的示例,用户可以输入数字然后拨出电话:

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() 方法:

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 的代码,在组合数字的时候会像这样:

Dialer::Dialer() {
    for (int digit = 0; digit <= 9; ++digit) {
        QString text = QString::number(digit);
        QPushButton* button = new QPushButton(text);
        button->setProperty("digit", digit);
        
        connect(button, &QPushButton::clicked,
                this, &Dialer::onClicked);
    }
    /* ... */
}
void Dialer::onClicked() {
    QPushButton* button = static_cast<QPushButton*>(sender());
    int digit = button->property("digit").toInt();
    mPhoneService->dial(digit);
}

我们可以使用 QSignalMapper 并去掉 Dialer::onClicked() 方法,但使用 Labmda 会更灵活更简单。我们只需要获取与按钮对应的数字,然后在 Lambda 中直接就能调用 mPhoneService->dial()。

Dialer::Dialer() {
    for (int digit = 0; digit <= 9; ++digit) {
        QString text = QString::number(digit);
        QPushButton* button = new QPushButton(text);
        
        connect(button, &QPushButton::clicked,
            [this, digit]() {
                 mPhoneService->dial(digit);
            }
        );
    }
    /* ... */
}

不要忘了对象的生命周期!

看这段代码:

void Worker::setMonitor(Monitor* monitor) {
    connect(this, &Worker::progress,
            monitor, &Monitor::setProgress);
}

在这个小例子中,有一个 Worker 实例来向 Monitor 实例报告进度。到目前为止,还没什么问题。

现在假设 Worker::progress() 有一个 int 型的参数,并且 monitor 的另一个方法需要使用这个参数值。我们会尝试这样做:

void Worker::setMonitor(Monitor* monitor) {
    // Don't do this!
    connect(this, &Worker::progress, [monitor](int value) {
        if (value < 100) {
            monitor->setProgress(value);
	} else {
	    monitor->markFinished();
	}
    });
}

看起来没问题……但是这段代码会导致崩溃!

Qt 的连接系统很智能,如果发送方和接收方中的任何一个被删除掉,它就会删除连接。在最初的 setMonitor() 中,如果 monitor 被删除了,连接也会被删除。但现在我们使用了 Lambda 来作为接收方: Qt 目前没有办法发现在 Lambda 中使用了 monitor。即使 monitor 被删除掉,Lambda 仍然会调用,结果应用就会在尝试引用 monitor 的时候发生崩溃。

为了避免崩溃发生,你要向 connect() 调用传入一个“context”参数,像这样:

void Worker::setMonitor(Monitor* monitor) {
    // Do this instead!
    connect(this, &Worker::progress, monitor,
            [monitor](int value) {
	if (value < 100) {
	    monitor->setProgress(value);
	} else {
	    monitor->markFinished();
	}
    });
}

这段代码中,我们把 monitor 作为上下文传入了 connect()。这不会对 Lambda 的执行造成影响,但是在 monitor 被删除之后,Qt 会注意到并解除 Worker::progress() 和 Lambda 之间的连接。

这个上下文还会用于检测连接是否在队列中。就像经典的 signal-slot 连接那样,如果上下文对象与发射信号的代码不在同一个线程,Qt 会将连接置入队列。

代替 QMetaObject::invokeMethod

你可能对一种异步调用 slot 的方法比较熟悉,它使用 QMetaObject::invokeMethod。先定义一个类:

class Foo : public QObject {
public slots:
    void doSomething(int x);
};

你可以在 Qt 中使用 QMetaObject::invokeMethod 在事件循环返回时调用 Foo::doSomething():

QMetaObject::invokeMethod(this, "doSomething",
                          Qt::QueuedConnection, Q_ARG(int, 1));

这段代码会工作,但是:

  • 语法太丑

  • 非类型安全

  • 你必须定义作为 slot 的方法

可以通过在 QTimer::singleShot() 中调用 Lambda 来代替上面的代码:

QTimer::singleShot(0, [this]() {
    doSomething(1);
});

这个效率会稍低一些,因为  QTimer::singleShot() 会在背后创建一个对象,不过,只要你不是要在一秒内调用很多次,这点性能损失可以忽略不计。显然利大于弊。

你同样可以在 Lambda 前面指定一个上下文,这在多线程中非常有用。但要小心:如果你使用低于 5.6.0 版本的 Qt,QTimer::singleShot() 有一个 BUG 在多线程中使用时会导致崩溃。我们找到了那个困难的办法……

关键点

  • 连接 Qt 对象的时候使用 Lambda 比使用调度方法更好

  • 在 connect() 调用中使用 Lambda 一定要有上下文

  • 按需获取变量

希望你能喜欢这篇文章,并希望你现在就用漂亮的 Lambda 语法替换掉古板的旧语法!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值