我有一个旧代码库,我开始使用 Qt 3.x 框架编写——在 Qt4 发布前不久。它还活着!我仍在努力,尽可能地与Qt和 C++ 保持同步,并且我仍在发布产品。多年来,我将代码库从 Qt4 转移到 Qt5,并转移到支持C++11的编译器上。我有时发现有点过分的一件事是为超短且不会被重用的东西声明插槽。
这是我的旧代码中的一个简化示例,它在项目“脏”(需要保存)时更改标题:
/// header
class myMainWindow ...
private slots:
void slotProjectDirtyChanged();
private:
void _setTitle( const QString &inTitle );
// implementation
...
connect( inProject, SIGNAL( signalDirtyChange( bool ) ),
SLOT( slotProjectDirtyChanged() ) );
...
void myMainWindow::slotProjectDirtyChanged()
{
QString name = mProject->name();
if ( mProject->dirty() )
name += '*';
_setTitle( name );
}
(旁白:如果您想知道我的命名约定,我在早期(c. 2000)使用 Qt 时开发了一种风格,其中信号被命名为signalFoo(),插槽被命名为slotFoo()。这样我一眼就知道是什么意图是。如果我要修改一个槽函数,我可能会花一分钟的时间来环顾四周,因为大多数 IDE 无法从语法上判断它在SLOT()
宏中的使用位置。在这种情况下,您必须以文本方式搜索它。)
由于 C++11 lambdas 和 Qt 的不断发展,这些短槽可以被更简洁的语法所取代。这避免了必须在类声明中声明方法并缩短实现代码。两个理想的目标!
让我们来看看。
摆脱 SIGNAL() 和 SLOT() 宏
Qt5 的第一个真正好处是我们可以从经典SIGNAL()
和SLOT()
宏转移到使用方法指针。
这为我们做了几件事:
- 使我们不必查找和填写方法的参数
- 缩短你的代码
- 当您搜索方法的使用时,IDE 可以识别连接调用中使用的方法
- 允许参数的隐式转换
- 在编译时而不是运行时捕获问题
这最后一个是最重要的。如果您尝试连接的两种方法不匹配,则根本无法编译。我可以权威地说,在编译时发现的问题可以为你节省很多很多时间的调试!
因此,如果我们将这个想法应用到我们的示例中,connect()
调用将变为:
connect( inProject, &myProject::signalDirtyChange,
&myMainWindow::slotProjectDirtyChanged );
已经有进步了!
这些 lambda 东西到底是什么?
lambda本质上是匿名函数的花哨名称。这些通常很短,可用于代码可能无法重用的插槽机制或回调。
lambda 函数的 C++11 规范如下所示:
[capture](parameters) -> return_type { function_body }
看起来很简单,但当你深入研究细节时,它们可能会相当复杂。
该capture
部分定义了如何捕获当前范围内的变量以供 function_body 使用。换句话说,它定义了哪些变量可用于当前范围内的 lambda 函数体。
我们将在这里使用的一个是[=]
按值从本地范围中捕获所有变量。您还可以不[]
捕获任何内容([&]
[this]
[foo, &bar]
它还需要可选parameters
,这是我们将信号参数传递给 lambda 的方式(见下文)。
return_type
并且function_body
是标准的C++。
如果我们将其应用于我们的示例,我们会得到如下内容:
connect( inProject, &myProject::signalDirtyChange, [=] () {
...do some stuff...
} );
在这里,我们将信号连接到一个匿名函数,该函数可以访问当前范围内的变量。我们省略了返回类型,因此void
可以推断。
(这是对 lambdas 的快速介绍。您可以在cppreference.com和这篇文章中找到更深入的描述。)
我们的最终结果
如果我们把所有这些放在一起,这就是我们的结果。我们去掉了头文件中的额外声明和实现中的额外函数。好多了。
// header
class myMainWindow ...
private:
void _setTitle( const QString &inTitle );
// implementation
...
connect( inProject, &myProject::signalDirtyChange, [=] () {
QString name = mProject->name();
if ( mProject->dirty() )
name += '*';
_setTitle( name );
} );
...
论据呢?
正如我之前提到的,lambdas 也可以接受参数,因此您可以将参数从信号传递到 lambda 函数。例如,我最近遇到了一个问题,我需要接受 a QRectF
,对其进行调整,然后再次发送。我是这样做的:
connect( mNavView, &myNavView::signalNavBoxChange, [=] ( const QRectF &inRect ) {
const QRectF cAdjusted( inRect.topLeft() / mRatio, inRect.bottomRight() / mRatio );
mView->slotMoveView( cAdjusted );
} );
您会注意到我也在使用封闭类中的mRatio和mView。因为我们捕获 using [=]
,所以 lambda 函数的主体可以访问封闭范围内的变量。
因为我只使用 的成员this
,所以我可以使用以下方法捕获[this]
:
connect( mNavView, &myNavView::signalNavBoxChange, [this] ( const QRectF &inRect ) {
const QRectF cAdjusted( inRect.topLeft() / mRatio, inRect.bottomRight() / mRatio );
mView->slotMoveView( cAdjusted );
} );
我不知道一个比另一个有什么优势——除了可能需要减少 lambda 函数体可以访问的范围。如果您对此有任何意见,请发表评论!
警告
根据您的工作方式,使用 lambda 可能会使测试变得更加困难。拥有可以在测试中调用的单独方法可能更可取。
在很多地方使用 lambdas 也很诱人。它非常强大。然而,我认为,将其限制在上述简单的事情上——替换短的一次性插槽、在传递数据之前调整数据或用于简单的回调——从长远来看将是最易于维护的。在我们与他们合作并使用生成的代码几年之前,我们不会知道……
希望对信息高速公路上的人有所帮助!
* 使用引用的 Lambda
正如罗纳尔多·纳扎里亚在评论中指出的那样:
尽管通过引用捕获(或按值捕获的指针),但您必须小心。如果插槽不在定义的范围内运行,您最终可能会引用由超出范围的变量导致的无效位置。