QtSeniorSystem design

Что такое идиома PIMPL (d-pointer) в Qt и зачем она используется?

PIMPL (d-pointer) — идиома, при которой все приватные поля класса вынесены в отдельную реализацию через указатель. В Qt это обеспечивает стабильность ABI и бинарную совместимость при изменении внутренней реализации.

Идиома PIMPL и d-pointer в Qt

PIMPL (Pointer to IMPLementation) — паттерн, в котором публичный класс содержит только указатель на приватную структуру с реальными данными. Qt использует расширенный вариант этой идиомы с макросами Q_D и Q_Q.

Зачем это нужно

  • Стабильность ABI: изменение приватных полей класса не меняет размер публичного класса и не требует перекомпиляции пользовательского кода.
  • Бинарная совместимость: пользователи Qt могут обновлять версию библиотеки без пересборки своего приложения.
  • Сокрытие деталей реализации: заголовочный файл не включает внутренние зависимости.
  • Ускорение компиляции: изменения в реализации не вызывают перекомпиляцию файлов, включающих заголовок.

Структура в Qt

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

// myclass.h — публичный заголовок
class MyClassPrivate; // forward declaration

class MyClass : public QObject {
    Q_OBJECT
    Q_DECLARE_PRIVATE(MyClass) // объявляет d_func()
public:
    explicit MyClass(QObject *parent = nullptr);
    ~MyClass();
    void doSomething();
private:
    QScopedPointer<MyClassPrivate> d_ptr; // d-pointer
};
// myclass_p.h — приватный заголовок (не для пользователей)
class MyClassPrivate {
    Q_DECLARE_PUBLIC(MyClass) // объявляет q_func()
public:
    explicit MyClassPrivate(MyClass *q) : q_ptr(q) {}
    void internalHelper();
    int privateData = 0;
    QString internalState;
private:
    MyClass *q_ptr; // обратный указатель (q-pointer)
};
// myclass.cpp — реализация
#include "myclass_p.h"

MyClass::MyClass(QObject *parent)
    : QObject(parent)
    , d_ptr(new MyClassPrivate(this)) // создаём приватную структуру
{}

MyClass::~MyClass() = default; // QScopedPointer освободит d_ptr

void MyClass::doSomething() {
    Q_D(MyClass); // получаем указатель d на MyClassPrivate
    d->internalHelper();
    d->privateData = 42;
}

void MyClassPrivate::internalHelper() {
    Q_Q(MyClass); // получаем указатель q на MyClass
    emit q->someSignal();
}

Макросы Q_D и Q_Q

Макрос Q_D(ClassName) разворачивается в:

ClassName##Private * const d = d_func();

Метод d_func() объявляется макросом Q_DECLARE_PRIVATE и возвращает типизированный указатель через static_cast. Это позволяет корректно работать с наследованием: у каждого уровня иерархии собственный приватный класс, но все они связаны через единый указатель d_ptr.

PIMPL в иерархии классов

// Дочерний класс расширяет приватную структуру
class DerivedClassPrivate : public MyClassPrivate {
public:
    explicit DerivedClassPrivate(DerivedClass *q)
        : MyClassPrivate(q) {} // передаём q-pointer родителю
    double derivedData = 0.0;
};

class DerivedClass : public MyClass {
    Q_OBJECT
    Q_DECLARE_PRIVATE(DerivedClass)
public:
    explicit DerivedClass(QObject *parent = nullptr)
        : MyClass(*new DerivedClassPrivate(this), parent) {}
};

Подводные камни

  • Забыть объявить деструктор в .cpp файле при использовании QScopedPointer<MyClassPrivate> — компилятор не видит определение MyClassPrivate в заголовке и не может сгенерировать деструктор.
  • Использовать d-pointer в inline-методах заголовочного файла — это раскрывает приватный заголовок пользователям, сводя на нет цель PIMPL.
  • Не передавать q-pointer при создании производного приватного класса — базовый q_ptr будет указывать на неправильный тип.
  • Использовать std::unique_ptr вместо QScopedPointer без явного деструктора — аналогичная проблема с incomplete type.
  • Прямой доступ к d_ptr.data() вместо Q_D() — при наследовании возникают проблемы с типами.
  • Забывать, что Q_DECLARE_PRIVATE создаёт const и неконстантные перегрузки d_func() — в const-методах нужно явно использовать Q_D(const MyClass).

Common mistakes

  • Объяснять PIMPL d-pointer только по синтаксису, без жизненного цикла и стоимости.
  • Игнорировать ошибки, null/empty состояния, порядок инициализации или режим сборки.
  • Давать пример, который работает в демо, но ломается при изменении владельца ресурса.
  • Смешивать signals/slots с generic callbacks и забывать про thread affinity.

What the interviewer is testing

  • Кандидат формулирует точную модель для PIMPL d-pointer, а не только определение.
  • Пример компилируем, безопасен по lifetime и соответствует версии технологии.
  • Названы trade-off, ограничения и диагностируемые симптомы ошибки.
  • Понимает границу между C++ кодом, runtime/framework metadata и editor/UI слоем.

Sources

Related topics