如何利用 C++ 的 Lambda 表达式提升 Qt 代码

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 语法替换掉古板的旧语法!

1 1 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部