Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does using C++ Lambda functions as slots in Qt help to preserve binary compatibility of a library?

I am concerned about making possible a library of widgets developed under Qt 5.9 to be upgraded in the future without having to recompile the code that already uses it. Of course I've started with the PImpl idiom and the Qt version of it described here and here.

However while trying to adapt my code, I came up with the idea, that instead of adding new data members and moving them to a separate private class, I could use the Qt's signal/slot mechanism with lambda functions and have only local variables. Let's illustrate the idea with the following example:

Variant A:

class Foo : public QWidget
{
    Q_OBJECT
public:
    explicit Foo(QWidget *parent = nullptr);

private:
    // A bunch of data members
    QPushButton *m_button;
    QLineEdit *m_lineEdit;
    QCheckBox *m_checkBox;
    QString m_str;

private slots:
    void on_pushButtonClicked();
    void on_checkBoxStateChanged(int state);
};

Foo::Foo(QWidget *parent) :
    QWidget(parent),
    m_button(new QPushButton("Click me", this));
    m_lineEdit(new QLineEdit(this)),
    m_checkBox(new QCheckBox(this)),
    m_str("Initial text")
{
    connect(button, &QPushButton::clicked, this, &Foo::on_pushButtonClicked);
    connect(checkBox, &QCheckBox::stateChanged, this, &Foo::on_checkBoxStateChanged);
}

Foo::on_pushButtonClicked()
{
    m_str = m_lineEdit->text();
    m_lineEdit->setDisabled(m_checkBox->isChecked());
}

Foo::on_checkBoxStateChanged(int state)
{
    m_button->setText(state == Qt::Checked ? m_str : "Click me")
}

Variant B:

class Foo : public QWidget
{
    Q_OBJECT
public:
    explicit Foo(QWidget *parent = nullptr);
};

Foo::Foo(QWidget *parent) : QWidget(parent)
{
    QPushButton *button = new QPushButton("Click me", this);
    QLineEdit *lineEdit = new QLineEdit(this);
    QCheckBox *checkBox = new QCheckBox(this);
    QString str("Initial text");

    connect(button, &QPushButton::clicked, [=](){
        str = lineEdit->text();
        lineEdit->setDisabled(checkBox->isChecked());
    });

    connect(checkBox, &QCheckBox::stateChanged, [=](int state){
        button->setText(state == Qt::Checked ? str : "Click me")
    });
}

So, for Variant B - apart from being more compact, it does not contain any class data members, so there are no variables to hide, hence no need for a D-pointer. The binary compatibility is still guaranteed though (or is it?), if in the future the constructor is reimplemented with additional local variables used in the same signal/slot manner. Am I right to think this will work or such an approach won't do the trick at all?

Note: For more info about using lambdas as slots in Qt check the comment by @Igor Tandetnik here.

like image 485
scopchanov Avatar asked Aug 24 '17 21:08

scopchanov


1 Answers

I came up with the idea, that instead of adding new data members and moving them to a separate private class [...]

That's the wrong way to think about it. The interface has no data members. Whatever members you have, go directly into the PIMPL. You don't "move" anything, you don't add them in the wrong place to start with.

Furthermore, heap allocations of members that have the same lifetime as the parent object is a premature pessimization. Store them by value in the PIMPL.

[...] I could use the Qt's signal/slot mechanism with lambda functions and have only local variables

This won't work as soon as you need to store something more than QObject children without abusing the property system.

It's not a flexible approach, and it's really not hard to do it correctly. Qt establishes all the necessary patterns. See this question for some details.

Classes that you don't intend to derive from don't need separate Class_p.h headers. You can add the ClassPrivate definition to the beginning of the Class.cpp file itself.

// Foo.h
#include <QWidget>
class FooPrivate;
class Foo : public QWidget {
  Q_OBJECT
  Q_DECLARE_PRIVATE(Foo)
  QScopedPointer<FooPrivate> const d_ptr;
public:
  explicit Foo(QWidget *parent = {});
  ~Foo();
protected:
  Foo(FooPrivate &, QWidget *parent = {}); // for expansion
};
// Bar.h
#include "Foo.h"
class BarPrivate;
class Bar : public Foo {
  Q_OBJECT
  Q_DECLARE_PRIVATE(Bar)
  Q_PROPERTY(int data READ data)
public:
  explicit Bar(QWidget *parent = {});
  ~Bar();
  int data() const;
protected:
  Bar(BarPrivate &, QWidget *parent = {}); // for expansion
};
// Foo_p.h
#include "Foo.h"

class FooPrivate {
  Q_DECLARE_PUBLIC(Foo)
  Q_DISABLE_COPY(Foo) // usually desired
  Foo * const q_ptr;
public:
  QVBoxLayout m_layout{q_ptr};
  QPushButton m_button{q_ptr->tr("Hello!")};
  QLineEdit m_lineEdit;
  QCheckBox m_checkBox{q_ptr->tr("Active")};

  void on_pushButtonClicked();
  void on_checkBoxStateChanged(int state);

  explicit FooPrivate(Foo *);
  virtual ~FooPrivate() {} // we're meant to be derived from!
};
// Bar_p.h
#include "Foo_p.h"
#include "Bar.h"

class BarPrivate : public FooPrivate {
  Q_DECLARE_PUBLIC(Bar)
public:
  int m_data = 44;

  explicit BarPrivate(Bar *);
};
// Foo.cpp
#include "Foo_p.h"    

Foo::Foo(QWidget * parent) :
  Foo(*new FooPrivate(this), parent)
{}

Foo::Foo(FooPrivate & d_ptr, QWidget * parent) :
  QWidget(parent),
  d_ptr(d_ptr)
{}

Foo::~Foo() {}

FooPrivate::FooPrivate(Foo * q_ptr) :
  q_ptr(q_ptr)
{
  m_layout.addWidget(&m_button);
  m_layout.addWidget(&m_lineEdit);
  m_layout.addWidget(&m_checkBox);
  connect(&m_button, &QPushButton::clicked, [=]{ on_pushButtonClicked(); });
  connect(&m_checkBox, &QCheckBox::stateChanged, [=](int s){ on_checkBoxStateChanged(s); });
}
// Bar.cpp
#include "Bar_p.h"

Bar::Bar(QWidget * parnet) :
  Bar(*new BarPrivate(this), parent)
{}

Bar::Bar(BarPrivate & d_ptr, QWidget * parent) :
  Foo(d_ptr, parent)
{}

Bar::~Bar() {}

BarPrivate::BarPrivate(Bar * q_ptr) :
  FooPrivate(q_ptr)
{}

int Bar::data() const {
  Q_D(const Bar);
  return d->m_data;
}
like image 186
Kuba hasn't forgotten Monica Avatar answered Sep 16 '22 15:09

Kuba hasn't forgotten Monica