QT5和C++ 11:Lambda是你的朋友(翻译文)

前言    

    自从Qt5发布以来,我一直在推迟对我一直在做的一个项目升级到Qt5。即使是像这样的版本,从Qt 4.7到Qt 5(没错,跳过了4.8),出于某种原因也不像暗示的那么简单。他们说: “只需改变包含和链接路径, 就会自行编译。” Psht,是正确的。别再上当了。

    在我使用Qt工作多年之后,我实现了飞跃,获得巨大的进步.我觉得C++和Qt现在是一起工作的,而不是仅仅帮助您开发更好的C++。我相信信号/槽机制已经在c++ 11 lambda函数中找到了它的灵魂伴侣。

这个信号/槽到底是什么?

    如果不使用Qt, 你可能根本就不在乎,但是Qt框架中对象之间的基本通讯机制是由信号(可以发出的事件)和槽(事件处理程序)定义的。

    作为快速介绍,使用以下示例(假设setupUi方法创建了三个QPushButton对象和一个QTextEdit对象):

class AMainWindow : public QWidget, public Ui::AMainWindow
{
Q_OBJECT
public:
   explicit AMainWindow(QWidget *parent = 0) : QWidget(parent)
   {
      // create the widget interface (add the buttons and text box)
      this->setupUi(this);

      // connect the signal/slots
      connect(pushButton, SIGNAL(clicked()), this, SLOT(SetTextOne()));
      connect(pushButton_2, SIGNAL(clicked()), this, SLOT(SetTextTwo()));
      connect(pushButton_3, SIGNAL(clicked()), this, SLOT(SetTextThree()));
}

public slots:
   void SetTextOne(void)
   {
      textEdit->setText("bonjour");
   }

   void SetTextTwo(void)
   {
      textEdit->setText("comment allez vous");
   }

   void SetTextThree(void)
   {
      textEdit->setText("pas trop mal, et vous?");
   }
};

 我们已经将每一个按钮的点击信号链接到这里定义的三个方法上。

    在connect方法调用中使用的SIGNAL和SLOT是连接函数名称的宏,出于我们的目的,先让我假设它是魔法。

 

那么,信号/槽机制有什么问题?

    这个没有什么损坏,对吧? 它的工作原理…我猜。我前面谈到的信号/槽宏“魔法”并不是那么神奇。这两个宏实际上都解析为一个字符串。

问题1:

它使用字符串在运行时解析连接。所以,如果你碰巧有一个槽,它接受一个字符串,而信号声明接受一个int,但你不知道它,直到你运行你的应用程序。除非您习惯JavaScript开发,否则这可能会让您措手不及。

问题2:

为什么我必须定义三个方法来做基本相同的事情?

 

在Qt5前

    在Qt5和c++ 11之前,我们可以用QSignalMapper类来做这样的事情:

class AMainWindow : public QWidget, public Ui::AMainWindow
{
   Q_OBJECT
public:
   explicit AMainWindow(QWidget *parent = 0) : QWidget(parent)
   {
       // create the widget interface
       this->setupUi(this);

       // use the QSignalMapper to pass custom string for each button
       // to a single slot
       QSignalMapper* mapper = new QSignalMapper();
       connect(mapper, SIGNAL(mapped(QString)), this, SLOT(SetText(QString)));

       connect(pushButton, SIGNAL(clicked()), mapper, SLOT(map()));
       mapper->setMapping(pushButton, "bonjour");

       connect(pushButton_2, SIGNAL(clicked()), mapper, SLOT(map()));
       mapper->setMapping(pushButton_2, "comment allez vous");

       connect(pushButton_3, SIGNAL(clicked()), mapper, SLOT(map()));
       mapper->setMapping(pushButton_3, "pas trop mal, et vous?");

   }

public slots:
   void SetString(QString text)
   {
       textEdit->setText(text);
   }
};

    使用QSignalMapper类我们去掉了三个槽函数,它们对不同的文本做了基本相同的操作,并用一个函数替换了它。但是你我都知道这感觉有点老套。QSignalMapper就像是一个真正问题的补丁。

 

添加C++ lambda函数    

    如果您一直关注c++的发展,您可能知道lambda函数和表达式。如果您不熟悉它们,可以通过搜索找到大量信息,但简单的回答是,它们基本上是内联的、未命名的函数。一般格式为:

[capture](parameters) { body };

 其中capture指定在函数声明的作用域中哪些可见的符号对lambda的主体是可见的,parameters是传递给lambda的参数列表,而body是函数的定义。

 

这对Qt意味着什么?

    要考虑的最重要的事情是,它们可以用作槽的函数指针。我们可以像这样连接一个槽:

connect(<pointer to source object>, <pointer to signal>, 
        <pointer to slot function>);

    首先,请注意,我们现在可以将实际指针传递到信号和槽,而不是仅仅使用信号和槽宏(如果需要,您可以仍然可以使用这些宏)。这意味着对connect的连接是在编译时期检查。不再运行程序并发现您使用了int作为槽,但是信号传递了一个字符串。

    其次,lambda基本上就是一个函数指针。现在考虑一下这如何改变我们的示例:

class AMainWindow : public QWidget, public Ui::AMainWindow
{
   Q_OBJECT
public:
   explicit AMainWindow(QWidget *parent = 0) : QWidget(parent)
   {
       // create the widget interface
       this->setupUi(this);

       // connect the signal to lambda (using [=] to capture variables 
       // used in the body by value)
       connect(pushButton, &QPushButton::clicked,
               [=]() { this->SetString("bonjour"); });
       connect(pushButton_2, &QPushButton::clicked,
               [=]() { this->SetString("comment allez vous"); });
       connect(pushButton_3, &QPushButton::clicked,
               [=]() { this->SetString("pas trop mal, et vous?"); });
   }

public slots:
   void SetString(QString text)
   {
       textEdit->setText(text);
   }
};

     注意: 如果您在Mac上使用Clang,您可能需要在.pro文件中添加“CONFIG += c++11”来启用c++11特性来支持lambda函数。

    上面例子与使用QSignalMapper比较。它只是更简洁,更容易理解。然而,我们不需要就此打住。由于我们的槽函数非常简单,而且真正重要的东西(我们正在设置的字符串)是在lambda中,所以甚至没有理由拥有它。

class AMainWindow : public QWidget, public Ui::AMainWindow
{
   Q_OBJECT
public:
   explicit AMainWindow(QWidget *parent = 0) : QWidget(parent)
   {
       // create the widget interface
       this->setupUi(this);

       // connect the signal to lambda (using [=] to capture variables
       // used in the body by value)
       connect(pushButton, &QPushButton::clicked,
               [=]() { textEdit->setText("bonjour"); });
       connect(pushButton_2, &QPushButton::clicked,
               [=]() { textEdit->setText("comment allez vous"); });
       connect(pushButton_3, &QPushButton::clicked,
               [=]() { textEdit->setText("pas trop mal, et vous?"); });
   }
};

    这是一个比我们原来有三个槽的类更优雅的解决方案。

 

附加说明

    当然,并非一切都是完美的。在使用指向函数的指针和lambdas作为槽时,有一些事情需要记住。首先,它有点复杂,因为您必须指定slot类的完整类型(如果您不使用lambda),但是较少的模糊性不会影响到任何人。

    但是有两个更大的问题:

(1) 函数指针和连接时不支持默认参数;

(2) 使用lambdas创建的槽在‘receiver’销毁时不会自动断开。

    第二个问题不一定是主要问题,因为使用lambda函数的事实表明您并不打算经常连接/断开连接(尽管仍然可以手动连接)。然而,第一个问题可能是很烦人的。考虑这样的写法:

connect(fromObject, &ASomeClass::someSignal, 
        toObject, &AAnotherClass::slotHandler);

其中信号和槽定义为:

void ASomeClass::someSignal(bool arg = true);
void AAnotherClass::slotHandler() { cout << “handled”; }

即使参数在考虑默认参数时匹配,connect方法也会抛出编译时错误。当然,一个简单的修复方法是这样使用lambda并且忽略信号参数:

connect(object, &ASomeClass::someSignal, 
        [=](bool arg) { toObject->slotHandler(); });

原文作者: 

Brian Poteat;

原文链接:

https://artandlogic.com/2013/09/qt-5-and-c11-lambdas-are-your-friend/

猜你喜欢

转载自blog.csdn.net/nicai_xiaoqinxi/article/details/85245843