Приватные слоты в паттерне 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;
      }
    }


    * This source code was highlighted with Source Code Highlighter.


    Вот теперь все ясно. Как вы видите эта директива заполняет нужные значения для определения метафункции, которая генерируется 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;
    };


    * This source code was highlighted with Source Code Highlighter.


    Реализация в 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


    * This source code was highlighted with Source Code Highlighter.


    ну и сам main.cpp:
    #include "myclassderived.h"
    #include <QApplication>

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


    * This source code was highlighted with Source Code Highlighter.


    Почему нужно было делать 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.

    Как всегда приглашаю вас посетить мой блог: erudenko.com. Там можно почитать буржуйскую версию этой статьи и разные дополнения. Можно там писать коменты, буду отвечать по возможности.

    Ну и по традиции исходники (исходники тестовой програмки полностью опубликованы выше, поэтому тут их не буду писать):
    habrhabr.pro:
    TEMPLATE = lib
    HEADERS += myclass.h \
      myclass_p.h \
      myclassderived.h \
      myclassderived_p.h
    SOURCES += myclass.cpp \
      myclassderived.cpp


    * This source code was highlighted with Source Code Highlighter.


    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


    * This source code was highlighted with Source Code Highlighter.


    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


    * This source code was highlighted with Source Code Highlighter.


    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;
    }


    * This source code was highlighted with Source Code Highlighter.


    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


    * This source code was highlighted with Source Code Highlighter.


    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


    * This source code was highlighted with Source Code Highlighter.


    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"


    * This source code was highlighted with Source Code Highlighter.
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 17
    • 0
      Спасибо за подробную статью
      • 0
        You are welcome.
        • 0
          Английский вариант статьи в блоге удваивает Ваши заслуги перед отечеством =)
          Меня пару месяцев назад невероятно интересовал весь этот Qt-шный PImpl, но тогда я так и не смог найти внятных ответов на мои вопросы. Ваши статьи многое прояснили. Надеюсь на этом Вы не остановитесь.
          • 0
            Спасибо. Честно говоря я собирался писать только на английском. Но потом захотелось стать частью хабра и решил написать статью в двух вариантах. По английски я пишу чтобы подтянуть свой английский, так как собираюсь сдавать IELTS экзамены. Одно из модулей сдачи — написание сочинения. Вот и тренируюсь. Плюс действительно действенно так как приходится вспоминать времена и некоторые слова (точнее часть слов из пассивного словаря переношу в активный).
            Но после того как я увидел резонанс (посещаемость моего блога сейчас 400 в день !!!) меня подбило занятся этим на «постоянной основе». Тем более, что рассказать мне еще много чего есть и я лично очень болею за будущее Qt и в моих же интересах вложить свои 5 копеек в популяризацию этого фреймворка. В этом случае востребованость Qt специалистов выростет, чему буду я очень рад :-)
            • +1
              да и кстати мой блог постиг хабраэффект :-) так как большинство посещений из России, Украины и Белорусии. Рейтинг идет в этом порядке, только между Россией и Украиной стоит США (сначала написал а потом понял двойственный смысл :-))
              Так что ребята спасибо за хабраэффект :-) Никогда не верил в разный там интернет пиар а тут увидел его в действии.
      • 0
        Прошу прощения, немного не по теме, но стало интересно, что за ключевое слово «signals: »?
        • +1
          Это Qt-шный по сути макрос. Область, где будут описаны функции в специальном унифицированном формате, которые потом можно потом выпонить (emit). А внешним по отношению к классу механизмом эти сигналы связываются со слотами (функциями-приемниками) других обработчиков, таким образом происходит взаимодействие между разными объектами. Весьма удобный унифицированный механизм. не без недостатков, конечно, но удобный.
          • 0
            Ясно, прикрутили события к старому доброму(?) С++ :)
            • +1
              да, типа того. Только не на уровне модификации компилятора как надо бы это сделать (расширение языка), а макросами и препроцессором. Но это религиозная тема совместимости, да:)
              • +1
                в бусте есть точно такое же «ключевое слово» (из-за этого при миксе буста и кутишного кода лучше пользоватся Q_SLOTS вместо slots). Кстати про сигналы и слоты тут перевод есть официальной доки в хабре habrahabr.ru/blogs/qt_software/50812/.

                Очень полезный механизм, четко спионереный у NextStep'а, GTK+ и boost тоже повзаимствовали его :-)
                • 0
                  Почитал, в чистом то, что у нас называется «события» :) Как-то так это выглядит в C#:

                  public class SomeClass {
                  public event Proc<int, SomType> SomeSignal;

                  }


                  void SomeSlot(int a, SomeType b){

                  }

                  var someObject = new SomeClass();

                  someObject.SomeSignal += SomeSlot;
                  • 0
                    по сути да. Методологическая основа у событий и у сигнал/слотов одинаковая.
          • 0
            а зачем выражение после emit брать в скобочки
            emit это тоже пустой макрос.
            • 0
              для того что все что в скобках макроса — это как-бы его параметры — это раз.
              А во вторых так проще парсить в moc.
            • +1
              Можно кстати будет потом вывернуть на изнанку moc предкомпилер и добавить возможность обработки собственного препроцессинга через плагины. Вот такая у меня идея вчера перед сном пришла. Правда прийдется систему плагинов делать чисто C++ ибо на момент сборки moc предполагается что qt еще не собран :-) Но это не остановит настоящего индейца.
              • +3
                Да кстати кому интересно. Я сейчас тут переписываюсь с Андре Поэтисом, который занимается разработкой Qt-creator по поводу gdbmacroshelper. Он поддерживает меня что он реализован не очень. Поэтому они ведут разработку по его улучшению и уже есть девелоперский вариант, который он выложил в репозиторий по моей просьбе. Суть в том, что теперь испльзуется не call команда gdb, а биндинг питоновго скриптинга в gdb.
                Моя основная идея состоит в том, чтобы расширить этот механизм для того чтобы можно было создавать дамперы своих кастомных классов. При существующей схеме единственный вариант — это линговать этот макрос с той библиотекой из которой вы хотите дампить класса. Так вот идея состоит в том чтоб придумать как это сделать так, чтобы можно было не прибегая к добавлению лишнего кода в исходники и не делая зависимым gdbmacroshelper от левого кода дать возможность людям писать собственные дамперы как можно проще.

                Вот напишу что и как я нарыл чуть позже, может кто посоветует чего-нибудь.
                • 0
                  В битве за самый выносящий мозг язык программирования мои любимые языки C++ и Haskell убьют друг друга :)

                  Спасибо за статью.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.