Skip to content

November 26, 2009

4

Приватные слоты в паттерне Pimpl от Qt.

Вступление.

Недавно я писал по поводу реализации паттерна Pimpl в библиотеке Qt и призывал людей следовать такому подходу при разработке их собственных бибиотек. Теперь я хочу поговорить о таком понятии, как приватные слоты и тем самым продолжить эту тему. Заключительной статьей на эту тему будет реализация механизма Implicit Sharing и shared d-pointer.

Что это и зачем это нужно.

Приватные слоты – это механизм дополняющий функционал d-указателей. Он позволяет реализовать слоты для приватного класса, даже если он не является наследником от QObject (обычно он им и не является), но для этого публичный класс должен быть наследником от QObject. Тоесть по факту создается некий приватный слот в публичном классе и он непосредственно дергает нужный метод приватного класса.
Зачем это нужно? Ну рассмотрим на примере. Есть класс QAbstractScrollArea. Он просто отображает некий виджет (viewport) и обеспечивает прокрутку. Прокрутка обеспечивается с помощью двух экземпляров класса QScrollBar. Сами эти скролбары он хранит в приватном классе. Теперь проблемма: как подключить сигнал от скроллбара об изменение его позиции с классом QAbstractScrollAreaPrivate, ведь он не является QObject’ом ? Сделать его наследником от QObject – лучше не делайте это :-) . Можно сделать слот в публичном классе и повесить на него, то в таком случае это не очень красиво – так как наружу выходят слоты от внутренней реализации. Вот ту Qt-шниками был придуман достаточно разумный и элегантный подход – приватные слоты.

Как это работает.

Для реализации приватного слота служит макрос Q_PRIVATE_SLOT. Ну по нашему обычаю залазим в исходники Qt и смотрим что он из себя представляет:

# define Q_PRIVATE_SLOT(d, signature)

Опа !!!! Пусто :-) . Но настоящие мужчины никогда не отчаиваются и лезут в исходники глубже, а точнее в исходники moc. Кто не знает, moc – это некая реализация некого парсера. Только немного хитрая. В отличие от инструментов использующих QLALR (QSA и QXmlStreamReader ) – эта штука оказалась своеобразной. Я не силент в парсерах поэтому более глобальный анализ провести не могу. Но он точно не похож на LALR потому-что я не нашел лексики его. Таблица токенов генерится некой утилитой generate_keywords, которую можно найти в исходниках moc в папке util.
Насколько я понял он просто генерит из любого текста токены и формирует из них структуру вида:

static const struct
{
   Token token;
   short next;
   char defchar;
   short defnext;
   Token ident;
}

Вот пример записей этого генератора:

    {Q_SIGNAL_TOKEN, 0, 83, 470, CHARACTER},
    {Q_SIGNALS_TOKEN, 0, 0, 0, CHARACTER},
    {Q_SLOT_TOKEN, 0, 83, 474, CHARACTER},
    {Q_SLOTS_TOKEN, 0, 0, 0, CHARACTER},
    {Q_PRIVATE_SLOT_TOKEN, 0, 0, 0, CHARACTER},

Кто хочет покапать дальше или вообще уже знает ответ – прошу высказыватся.
Кстати очень просто оказалось это все дело расширить :-) можно добавить свой препроцессинг. Вот что значит писать красиво.

в token.cpp находим наш токен:

        case Q_PRIVATE_SLOT_TOKEN: return "Q_PRIVATE_SLOT_TOKEN";

И далее в реализации метода MOC::parse() находим что же он делает, когда встречает этот парсер:

              case Q_PRIVATE_SLOT_TOKEN:
                    parseSlotInPrivate(&def, access);
                    break;

Ну вот и добрались до сути, она находится в методе parseSlotInPrivate(&def, access);
Вот и она:

void Moc::parseSlotInPrivate(ClassDef *def, FunctionDef::Access access)
{
    next(LPAREN);
    FunctionDef funcDef;
    next(IDENTIFIER);
    funcDef.inPrivateClass = lexem();
    // also allow void functions
    if (test(LPAREN)) {
        next(RPAREN);
        funcDef.inPrivateClass += "()";
    }
    next(COMMA);
    funcDef.access = access;
    parseFunction(&funcDef, true);
    def->slotList += funcDef;
    while (funcDef.arguments.size() > 0 && funcDef.arguments.last().isDefault) {
        funcDef.wasCloned = true;
        funcDef.arguments.removeLast();
        def->slotList += funcDef;
    }
}

Вот теперь все ясно. Как вы видите эта директива заполняет нужные значения для определения метафункции, которая генерируется moc’ом ( таким же образом как и для обычных слотов ). А пустой дефайн просто делает эту запись пустой, когда moc прошел и настает время предкомпилятору. Тоесть на момент стандартной компиляции там уже пусто, но реализация слота сгенерирована в moc файле. Далее я покажу как она выглядит в нашем примере.
Ну вобщем разобрались с тем как это устроено, далее будет смотреть как это использовать.

Как реализовать это в своем коде.

Тут в дейсвтительности все очень просто. В качестве примера берем классы из моей предыдущей статьи . И добавляем новый функционал.
1. Добавляем метод void _q_boo() в MyClassPrivare:

class MyClassPrivate
{
	Q_DECLARE_PUBLIC(MyClass);
public:
	MyClassPrivate();
	virtual ~MyClassPrivate();

	void foo();
	void _q_boo();
	int i;
	MyClass * q_ptr;
};

Реализация в myclass.cpp будет выглядеть таким образом:

void MyClassPrivate::_q_boo()
{
	qDebug()<<i;
	QCoreApplication::exit(1);
}

Не забудте добавить QDebug и QCoreApplication в инклуды.
По поводу имени _q_boo. Это правило именования приватных слотов от qt. Имя должно начинатся на “_q_”. Тогда при просмотре объявления приватного класса можно определить что это слот.
QCoreApplication::exit(1) – я добавил для того чтобы при сраватывании мы завершили основной цикл приложения (далее напишу листинг этого приложения для тестирования нашей библиотеки).
2. Для большей запутаности ( а то сильно просто все получается ) давайте дерганье этого слота реализуем в наследнике – MyClassDerived. Для этого объявим у него в приватной секции такой вот макрос:

	Q_PRIVATE_SLOT(d_func(),void _q_boo());

Из вышеуказанного реверс инжиниринга moc мы тут видим, что нам нужен указатель на класс и имя его метода. По факту это простой текстовый анализ на основании которого потом сгенерируется код moc_myclassderived.cpp.
3. Попробуем собрать наш проект:

moc_myclassderived.cpp: In member function ‘virtual int MyClassDerived::qt_metacall(QMetaObject::Call, int, void**)’:
moc_myclassderived.cpp:70: error: invalid use of incomplete type ‘struct MyClassDerivedPrivate’
myclassderived.h:5: error: forward declaration of ‘struct MyClassDerivedPrivate’

Это нормально, ведь moc ничего не знает о нашем Private классе, ведь он инклудид в себя только тот заголовочный файл, на основании которого он был сгенерированый. А в публичном заголовочном файле только forward объявление. Так как moc – это по сути добавка к нашей реализации, просто включим его в конец нашего .cpp файла:

..................
#include "moc_myclassderived.cpp"

Теперь запускаем сборку и … Опять та же ошибка !!! Не отчаивайтесь. Просто сделайте make distclean и соберите снова.
Ну вот вобщем и все … аааа конечно, пример использования. Тогда идем дальше.
Как и общела смотрим, что же нагенерил нам moc:

int MyClassDerived::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
    _id = MyClass::qt_metacall(_c, _id, _a);
    if (_id < 0)
        return _id;
    if (_c == QMetaObject::InvokeMetaMethod) {
        switch (_id) {
        case 0: signal2((*reinterpret_cast< int(*)>(_a[1]))); break;
        case 1: d_func()->_q_boo(); break;
        default: ;
        }
        _id -= 2;
    }
    return _id;
}

Обратите внимание: когда к нам приходит запрос на вызов слота с _id=1, то мы дергаем метод приватного класса напрямую, без никаких потерь, как будто эта система метавызовов является родной для приватного класса а не для публичногою (немного завираю, так как все таки небольшой оверхед есть в преобразовании типов, во время вызова функции d_func() ).

4. Давайте запустим таймер, чтоб он дернул наш метод при создании класса MyClassDerived через одну секунду.
В первую очередь объявим init() метод в MyClassDerivedPrivate.
Реализация в myclassderived.cpp выглядит таким образом:

void MyClassDerivedPrivate::init()
{
	Q_Q(MyClassDerived);
	QTimer::singleShot(1000,q,SLOT(_q_boo()));
}

Ну и добавим вызов инициализации в наш конструктор (раньше они у нас были пустые):

MyClassDerived::MyClassDerived(QObject *parent)
	:MyClass(*new MyClassDerivedPrivate(), parent)
{
  Q_D(MyClassDerived);
  d->init();

}

MyClassDerived::MyClassDerived(MyClassDerivedPrivate &dd, QObject * parent)
		:MyClass(dd, parent)
{
	Q_D(MyClassDerived);
	d->init();
}

Конечно в данном случае можно было не городить конструкцию с init(), просто бы в каждом конструкторе бы была такая запись:

    QTimer::singleShot(1000,this,SLOT(_q_boo()));

Но я хотел показать подход, когда в инициализации необходимо выполнить более чем одно действие.

Не поверите :-) Но это все. В следующем пункте покажу программку, которая это все оттестирует.

Тестируем наш код

Ну вот создаем новый проект (в моем случае я его сделал в подкаталоге main моего проекта habrahabra):
main.pro

TEMPLATE = app
LIBS += -lhabrhabr -L..
INCLUDEPATH += ../
TARGET = main

SOURCES += main.cpp

ну и сам main.cpp:

#include "myclassderived.h"
#include <QApplication>

int main(int argc, char ** argv) {
	QApplication a(argc,argv);
	MyClassDerived d(0);
	return a.exec();
}

Почему нужно было делать QApplication? Да потому как система сигналов и слотов не будет работать без цикла выполнения потока в контексте которого он будет выполнятся (в нашем случае mainloop’a).
Собираем, запускаем, получаем результат:

7
exit-code:1

Урааа!!! Все работает.

Заключение.

Может вас испугало что я так много написал по этой теме ? В таком случае могу вас успокоить, все намного проще чем вам кажется. Просто я хотел показать как работает этот механизм, а не просто инструкции по применению вроде: “Вставтье это сюда и это сюда и ура у вас все работает”. В действительности необходимо проделать всего три шага.
1. Объявить макрос Q_PRIVATE_SLOT(d_func(),_q_method());
2. Реализовать этот метод в приватном классе.
3. Добавить в конец cpp файла #include “moc_classname.cpp”

И все !!! Все просто.

Важно: я уже указывать порядок следования заголовочных файлов в статье касательно pimpl. Но тут напомню, что все приватные заголовочные файлы должны строго следовать за публичными и в списке HEADERS они должны идти в порядке включения. В нашем примере derived класс включает в себя заголовки базового класса, поэтому в списке HEADERS файла проекта он идет после заголовков родителя.
Ну и так же как и макросы d-указателя, макросы приватного слота не являются частью публичного API и могут быть изменены в любой момент. Но не пугайтесь, я успокаивал по этому поводу в статье по pimpl.

Ну и по традиции исходники (исходники тестовой програмки полностью опубликованы выше, поэтому тут их не буду писать):
habrhabr.pro:

TEMPLATE = lib
HEADERS += myclass.h \
    myclass_p.h \
	myclassderived.h \
    myclassderived_p.h
SOURCES += myclass.cpp \
    myclassderived.cpp

myclass.h

#ifndef MYCLASS_H
#define MYCLASS_H

#include <QObject>

class MyClassPrivate;
class MyClass : public QObject
{
Q_OBJECT
public:
    explicit MyClass(QObject *parent = 0);
	int foo() const;
signals:
	void signal(int);
protected:
	MyClassPrivate * const d_ptr;
	MyClass(MyClassPrivate &dd, QObject * parent);
private:
	Q_DECLARE_PRIVATE(MyClass);
};

#endif // MYCLASS_H

myclass_p.h

#ifndef MYCLASS_P_H
#define MYCLASS_P_H
#include "myclass.h"

class MyClassPrivate
{
	Q_DECLARE_PUBLIC(MyClass);
public:
	MyClassPrivate();
	virtual ~MyClassPrivate();

	void foo();

	void _q_boo();

	int i;
	MyClass * q_ptr;
};

#endif // MYCLASS_P_H

myclass.cpp

#include "myclass.h"
#include "myclass_p.h"
#include <QDebug>
#include <QCoreApplication>
MyClassPrivate::MyClassPrivate()
{
   i = 5;
}

MyClassPrivate::~MyClassPrivate()
{
	//nothing to do
}

void MyClassPrivate::foo()
{
	Q_Q(MyClass);
	emit(q->signal(i));
}

void MyClassPrivate::_q_boo()
{
	qDebug()<<i;
	QCoreApplication::exit(1);
}

MyClass::MyClass(QObject *parent)
	:QObject(parent)
	,d_ptr(new MyClassPrivate())
{
	Q_D(MyClass);
	d->q_ptr = this;
}

MyClass::MyClass(MyClassPrivate &dd, QObject * parent)
	:QObject(parent)
	,d_ptr(&dd)
{
	Q_D(MyClass);
	d->q_ptr = this;
}

int MyClass::foo() const
{
	Q_D(const MyClass);
	return d->i;
}

myclassderived.h

#ifndef MYCLASSDERIVED_H
#define MYCLASSDERIVED_H
#include "myclass.h"

class MyClassDerivedPrivate;
class MyClassDerived : public MyClass
{
Q_OBJECT
public:
    explicit MyClassDerived(QObject *parent = 0);
signals:
	void signal2(int);
protected:
	MyClassDerived(MyClassDerivedPrivate &dd, QObject * parent);
private:
	Q_DECLARE_PRIVATE(MyClassDerived);
	Q_PRIVATE_SLOT(d_func(),void _q_boo());
};

#endif // MYCLASSDERIVED_H

myclassderived_p.h

#ifndef MYCLASSDERIVED_P_H
#define MYCLASSDERIVED_P_H

#include "myclassderived.h"
#include "myclass_p.h"

class MyClassDerivedPrivate: public MyClassPrivate
{
	Q_DECLARE_PUBLIC(MyClassDerived);
public:
	MyClassDerivedPrivate();
	virtual ~MyClassDerivedPrivate();

	void foo2();
	void init();
	int j;
};

#endif // MYCLASSDERIVED_P_H

myclassderived.cpp


#include "myclassderived.h"
#include "myclassderived_p.h"
#include <QTimer>

MyClassDerivedPrivate::MyClassDerivedPrivate()
{
	j=6;
	i=7;
}

MyClassDerivedPrivate::~MyClassDerivedPrivate()
{

}

void MyClassDerivedPrivate::foo2()
{
	Q_Q(MyClassDerived);
	emit(q->signal2(j));
	emit(q->signal(j));
}

void MyClassDerivedPrivate::init()
{
	Q_Q(MyClassDerived);
	QTimer::singleShot(1000,q,SLOT(_q_boo()));
}

MyClassDerived::MyClassDerived(QObject *parent)
	:MyClass(*new MyClassDerivedPrivate(), parent)
{
  Q_D(MyClassDerived);
  d->init();

}

MyClassDerived::MyClassDerived(MyClassDerivedPrivate &dd, QObject * parent)
		:MyClass(dd, parent)
{
	Q_D(MyClassDerived);
	d->init();
}

#include "moc_myclassderived.cpp"

Unique visitors to post: 77

4 Comments Post a comment
  1. asvil
    Feb 12 2010

    Теперь я уже прозрел к приватным слотам.
    ……..конструкторе бы была такая запись:
    QTimer::singleShot(1000,this,SLOT(_q_boo()));…….

    Поправте пожалуйста на:
    QTimer::singleShot(1000,q,SLOT(_q_boo()));

    Reply
  2. mAX
    Mar 27 2010

    Здравствуйте! У меня вот вопрос возник)
    Пишу код, в коде получается много инклудов и из-за этого “раздутый” проект очень долго компилится при изменении *.hpp файлов. Даже если там просто приватную переменную добавили.
    Штука про которую Вы написали мне очень помогла!!) Это просто супер, хоть и немного не удобно.

    Вопрос такой: Я сделал приватный класс для QWidget класса, ну или скажем для QMainWindow, думаю это не важно. Т.к. у Вас в примере был конструкор explicit MyClass(QObject *parent = 0); а сам класс от QObject, то в моем случае класс наследуется от, например, QMainWindow, тогда протектед конструктор для публичного класса… морды чеширского кота) я пишу так:
    MainFrame::MainFrame(MainFramePrivate &dd, QWidget * parent) : QMainWindow(parent) ,d_ptr(&dd) …
    Правильно ли я его записал?

    У меня вроде все работает с приватными переменными, но со слотами не работает… прога собирается, но они просто не срабатывают.

    В приватном классе я объявил
    class MainFramePrivate {
    Q_DECLARE_PUBLIC(MainFrame);
    public:
    MainFramePrivate();
    virtual ~MainFramePrivate();
    MainFrame * q_ptr;
    void Init(void);
    boost::shared_ptr myWidget;
    void _q_showWidget();

    // пункт меню, которое инициализируется в Init()
    QAction *actionShow;

    // прочая фигня… кнопки, меню и т.д.
    };

    реализация методов и конструкторов выглядит примерно так:

    void MainFramePrivate::Init(void) {
    Q_Q(MainFrame);
    actionShow = new QAction(q);
    // Тут инициализирую менюшку… с моей кнопкой
    q->setMenuBar(menubar);

    // можно ли тут вызвать QObject::connect чтобы связать сигнал объекта actionShow со слотом _q_showWidget?? Если можно, то подскажите, пожалуйста, как.
    }

    void MainFramePrivate::_q_showWidget() {
    myWidget->show();
    }

    MainFrame::MainFrame(QWidget *parent) : QMainWindow(parent) , d_ptr(new MainFramePrivate()) {
    Q_D(MainFrame);
    d->q_ptr = this;
    d->Init();

    // так работает… но этот приватный слот принадлежит публичному классу, и соответственно его объявление в hpp файле “портит”
    // QObject::connect(d->actionShow, SIGNAL(activated()), this, SLOT(showWidget()));

    // а так не работает…
    // QObject::connect(d->actionShow, SIGNAL(activated()), this, SLOT(d->_q_showWidget()));
    }

    MainFrame::MainFrame(MainFramePrivate &dd, QWidget * parent) : QMainWindow(parent) ,d_ptr(&dd) {
    Q_D(MainFrame);
    d->q_ptr = this;
    }

    void MainFrame::showWidget() {
    Q_D(MainFrame);
    d->myWidget->show();
    }

    Не знаю будет ли тут удобным смотреть код, но если нормально запостится, то посмотрите в чем ошибка, пожалуйста. Спасибо!

    Reply
  3. mAX
    Mar 27 2010

    Да, и кстати… можно ли использовать слоты приватного класса виджета из QtDesigner’а при создании виджетов? Я не знал как это сделать, поэтому пришлось весь сгенерированный код внести в приватный класс.

    Reply
  4. Mar 30 2010

    // так работает… но этот приватный слот принадлежит публичному классу, и соответственно его объявление в hpp файле “портит”
    // QObject::connect(d->actionShow, SIGNAL(activated()), this, SLOT(showWidget()));

    так правильно, по факту слоты принадлежат публичному классу, так как слоты должны быть только у наследников QObject, к коим приватный класс не относится
    Поэтому коннектить сигнал нужно к слоту публичного класса, а вот реализация этого слота нахоится в приватном классе, Получается интерфейс слота в публичном а реализация в приватном.

    И ничто так никого не портит :-)

    Слоты приватного класса можно естественно использовать в дезайнере, так как по факту это слоты публичного класса, которые дизайнеру соответсвенно доступны.

    да и небольшая ремарка к коду

    boost::shared_ptr myWidget;
    нецелесообразно “мешать” буст в данном случае с Qt, так как в Qt есть QSharedPointer который в данном случае уместней.

    Reply

Share your thoughts, post a comment.

(required)
(required)

Note: HTML is allowed. Your email address will never be published.

Subscribe to comments