Идиома 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.
Задание
ЛегкоДавайте создадим обработчик защищенных сообщений, используя идиому 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: Считайте два входных значения:- Строку сообщения (может содержать пробелы)
- Ключ шифрования (целое число)
Создайте объект
SecureMessage, установите ключ и выведите результаты:- Выведите
Original:, а затем исходное сообщение - Выведите
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;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыСборка и компиляция C++Заголовочные файлы и файлы исходного кодаПространства имен и область видимостиВведение в ООП на C++Классы и объектыУказатель 'this'Методы (функции-члены)Атрибуты (члены данных)Основы конструкторов и деструкторовИтоги — Простой калькулятор4Свойства классов
Члены экземпляра против статическихГеттеры и сеттерыКонстантные функции-членыКлючевое слово mutableСтатические методы и переменныеДружественные функции и классыИтоги — Менеджер банковских счетов7Наследование
Основы наследованияУровни доступа при наследованииПорядок вызова конструкторов и деструкторовПереопределение методовВиртуальные функции и VTableМножественное наследованиеВиртуальное наследованиеИтоги — Иерархия сотрудников10Обзор STL
Обзор и философия STLКонтейнеры STLИтераторыАлгоритмы STLФункторы и лямбда-выраженияИтоги: частота слов13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonFactory и Abstract FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy2Управление памятью
Стек и кучаУказатели и ссылкиДинамическая память (new/delete)Умные указатели в C++RAII в C++Итоги — Менеджер динамического массива5Инкапсуляция
Спецификаторы доступа в C++Спецификаторы доступа: подробный разборСокрытие данныхStruct против ClassВложенные и внутренние классыИтоги — Система учета студентов8Полиморфизм
Полиморфизм компиляции и времени выполненияПерегрузка функцийВиртуальные функции: повторениеЧистые виртуальные функцииАбстрактные классыПроектирование интерфейсов на C++Dynamic Casting и RTTIИтоги: Калькулятор фигур11Продвинутые концепции ООП
Композиция против наследованияМиксины через CRTPИдиома PimplСтирание типовEnum Classes и строгая типизацияОбработка исключений в ООППользовательские иерархии исключений14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикRAII как паттерн3Конструкторы и деструкторы
Конструктор по умолчаниюПараметризованный конструкторКонструктор копированияКонструктор перемещенияСписки инициализации конструктораДелегирующие конструкторыГлубокое погружение в деструкторыПравило трех / пяти / нуляПовторение — класс String6Перегрузка операторов
Введение в перегрузку операторовПерегрузка арифметических операторовПерегрузка операторов сравненияОператоры потоковПерегрузка оператора присваиванияПерегрузка операторов [] и ()Операторы преобразования типовИтоги — класс Matrix9Шаблоны
Шаблоны функцийШаблоны классовСпециализация шаблоновВариативные шаблоныОсновы SFINAE и Type TraitsИтоги — Обобщенный контейнер