Menu
Coddy logo textTech

Идиома Pimpl

Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 78 из 104.

Идиома Pimpl (Pointer to Implementation — указатель на реализацию) — это техника, которая скрывает детали реализации класса, перенося их в отдельный, предварительно объявленный класс. Это сокращает зависимости компиляции и позволяет по-настоящему скрыть приватные члены из заголовочного файла.

Основная идея проста: вместо того чтобы объявлять закрытые члены непосредственно в вашем классе, вы объявляете указатель на класс реализации, который определен только в файле исходного кода:

// Widget.h
#include <memory>

class Widget {
public:
    Widget();
    ~Widget();
    void doSomething();
    
private:
    class Impl;  // Опережающее объявление
    std::unique_ptr<Impl> pImpl;
};
// Widget.cpp
#include "Widget.h"
#include <iostream>

class Widget::Impl {
public:
    int data = 42;
    void process() { std::cout << "Processing: " << data << "\n"; }
};

Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;

void Widget::doSomething() { pImpl->process(); }

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

Обратите внимание, что деструктор должен быть определен в исходном файле (даже если он задан по умолчанию), так как unique_ptr требует полного определения типа Impl для его удаления. Это распространенная ошибка при первом использовании Pimpl.

challenge icon

Задание

Легко

Давайте создадим обработчик защищенных сообщений, используя идиому Pimpl, чтобы скрыть детали реализации шифрования из заголовочного файла. Это демонстрирует, как Pimpl создает «брандмауэр компиляции» — любой, кто подключает ваш заголовочный файл, не увидит внутреннюю логику обработки сообщений.

Вы организуете свой код в трех файлах:

  • SecureMessage.h: Определите публичный интерфейс для вашего класса SecureMessage.

    Ваш класс должен иметь опережающее объявление класса Impl и std::unique_ptr на него. Публичный интерфейс должен включать:

    • Конструктор, принимающий const std::string& для исходного сообщения
    • Деструктор (должен быть объявлен здесь, а определен в .cpp файле)
    • Метод setKey(int key) для установки ключа шифрования
    • Метод getEncrypted(), который возвращает зашифрованное сообщение в виде std::string
    • Метод getOriginal(), который возвращает исходное сообщение

    Заголовочный файл должен показывать только публичный интерфейс — никакие детали реализации того, как работает шифрование, не должны быть здесь видны.

  • SecureMessage.cpp: Определите вложенный класс Impl и реализуйте все методы.

    Ваш класс Impl должен хранить исходное сообщение, ключ шифрования (по умолчанию 0) и обрабатывать саму логику шифрования. Используйте простой шифр Цезаря для шифрования: сдвигайте каждый символ на значение ключа. Например, при ключе 3 'a' становится 'd', а 'z' циклически переходит в 'c'.

    Шифрование должно затрагивать только строчные буквы (a-z), оставляя все остальные символы без изменений. Не забудьте определить деструктор здесь (даже если он помечен как defaulted), так как unique_ptr требует наличия полного типа Impl.

  • main.cpp: Считайте два входных значения:
    1. Строку сообщения (может содержать пробелы)
    2. Ключ шифрования (целое число)

    Создайте объект SecureMessage, установите ключ и выведите результаты:

    1. Выведите Original: , а затем исходное сообщение
    2. Выведите Encrypted: , а затем зашифрованное сообщение

Например, при входных данных hello world и 3:

Original: hello world
Encrypted: khoor zruog

При входных данных xyz abc и 5:

Original: xyz abc
Encrypted: cde fgh

Обратите внимание, что заголовочный файл ничего не сообщает о реализации шифра Цезаря — в этом и заключается сила Pimpl. Если вы позже измените алгоритм шифрования на другой, потребуется перекомпиляция только SecureMessage.cpp, а не всех файлов, которые подключают этот заголовок.

Шпаргалка

Идиома Pimpl (Pointer to Implementation — указатель на реализацию) скрывает детали реализации, перенося их в отдельный класс, определенный только в файле исходного кода, создавая тем самым «барьер компиляции» (compilation firewall).

Базовая структура с опережающим объявлением в заголовочном файле:

// Widget.h
#include <memory>

class Widget {
public:
    Widget();
    ~Widget();  // Должен быть объявлен в заголовочном файле, определен в .cpp
    void doSomething();
    
private:
    class Impl;  // Опережающее объявление
    std::unique_ptr<Impl> pImpl;
};

Реализация в файле исходного кода:

// Widget.cpp
#include "Widget.h"

class Widget::Impl {
public:
    int data = 42;
    void process() { /* реализация */ }
};

Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;  // Должен быть определен в .cpp

void Widget::doSomething() { pImpl->process(); }

Ключевые моменты:

  • Деструктор должен быть определен в файле исходного кода (даже если он помечен как default), потому что unique_ptr требует наличия полного типа Impl для его удаления
  • Изменения в Impl требуют перекомпиляции только файла исходного кода, а не файлов, которые подключают заголовочный файл
  • Обеспечивает барьер компиляции и бинарную совместимость

Попробуйте сами

#include <iostream>
#include <string>
#include "SecureMessage.h"

int main() {
    // Прочитать сообщение (может содержать пробелы)
    std::string message;
    std::getline(std::cin, message);
    
    // Прочитать ключ шифрования
    int key;
    std::cin >> key;
    
    // TODO: Создать объект SecureMessage с этим сообщением
    
    // TODO: Установить ключ шифрования
    
    // TODO: Вывести "Original: " и следом исходное сообщение
    
    // TODO: Вывести "Encrypted: " и следом зашифрованное сообщение
    
    return 0;
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование