7

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.

scopchanov
  • 6,933
  • 10
  • 30
  • 56
  • I don't think that'll compile. `str` would be const inside lambda. Even if it does compile, note that you capture by value: `str` in those two different lambdas refers to different, independent objects. You'd have to hold everything by a heap-allocated pointer - a `pimpl` on steroids. – Igor Tandetnik Aug 25 '17 at 23:53
  • @IgorTandetnik, I've got your point. However, the sole reimplementation of this constructor should not break the binary compatibility, should it? – scopchanov Aug 26 '17 at 00:01
  • I'm not quite sure what you mean by "binary compatibility" here. As long as you don't modify the header where `Foo` class is defined, you won't need to recompile sources using `Foo`, if that's what you are asking. That said, you'll have a hard time writing any significant piece of code in the style of Variant B. Say, in a normal class you can have a private member function that you can call from multiple places. How do you plan to reuse code now? Create a lambda in the constructor for every "member function", and have all connection handlers capture it? It'll get pretty awkward pretty fast. – Igor Tandetnik Aug 26 '17 at 00:14
  • @IgorTandetnik, Yes, that is what I've asked. As for the hard time - that's for sure. I mean it's not a straight-forward task, but neither is the PImpl, at least in Qt. I am having hard time already converting my code to use d-pointers, not to mention the attempts to extend the functionality of the Qt's stock widgets without having to rewrite everything from scratch and still follow this design pattern (which is a different topic and I would better post it as a diffrent question). – scopchanov Aug 26 '17 at 00:25

1 Answers1

3

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;
}
Kuba hasn't forgotten Monica
  • 88,505
  • 13
  • 129
  • 275
  • First of all, thank you for the explaination and the good example provided! I have to add though, that I have checked the link before and in this particullar question it doesn't go about how to use PIMPL in Qt, but could it be replaced with other means (as extensive use of lambdas for example). As far as I understand, your opinion is - not, it can't be substituted. As for "moving" I mean redesigning my library, which does not use the idiom at the moment, hence should be rewritten in a similar manner. I have specific problems with that though, which I think is worth opening a new question. – scopchanov Aug 29 '17 at 22:35
  • @scopchanov Yes, please open another question. It'll be interesting to see what else you run into. – Kuba hasn't forgotten Monica Aug 30 '17 at 12:50
  • I will do it tonight and mention your name to let you know. I am eager to hear your opinion on that. – scopchanov Aug 30 '17 at 13:36