Idiom Pimpl
Część sekcji Programowanie obiektowe ścieżki C++ w Coddy. Lekcja 78 z 104.
Idiom Pimpl (Pointer to Implementation) to technika ukrywająca szczegóły implementacji klasy poprzez przeniesienie ich do osobnej klasy zadeklarowanej w przód. Zmniejsza to zależności kompilacji i sprawia, że prywatne składowe pozostają naprawdę ukryte w pliku nagłówkowym.
Główna idea jest prosta: zamiast deklarować prywatne składowe bezpośrednio w swojej klasie, deklarujesz wskaźnik do klasy implementacyjnej zdefiniowanej wyłącznie w pliku źródłowym:
// Widget.h
#include <memory>
class Widget {
public:
Widget();
~Widget();
void doSomething();
private:
class Impl; // Deklaracja wyprzedzająca
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(); }Kluczowe korzyści to izolacja kompilacji i zgodność binarna. Gdy zmienisz klasę Impl, ponownej kompilacji wymaga tylko plik źródłowy — nie każdy plik, który dołącza plik nagłówkowy. To znacznie przyspiesza kompilację dużych projektów.
Pamiętaj, że destruktor musi być zdefiniowany w pliku źródłowym (nawet jeśli jest domyślny), ponieważ unique_ptr potrzebuje pełnego typu Impl, aby go usunąć. To częsta pułapka przy pierwszym użyciu Pimpl.
Wyzwanie
ŁatwyStwórzmy moduł obsługi bezpiecznych wiadomości, wykorzystując idiom Pimpl do ukrycia szczegółów implementacji szyfrowania w pliku nagłówkowym. To pokazuje, jak Pimpl tworzy zaporę kompilacji: osoby dołączające Twój plik nagłówkowy nie zobaczą wewnętrznego działania przetwarzania wiadomości.
Rozmieścisz kod w trzech plikach:
SecureMessage.h: Zdefiniuj publiczny interfejs swojej klasySecureMessage.Twoja klasa powinna mieć zadeklarowaną z wyprzedzeniem klasę
Imploraz wskaźnikstd::unique_ptrdo niej. Publiczny interfejs powinien zawierać:- Konstruktor przyjmujący
const std::string&z oryginalną wiadomością - Destruktor (musi być zadeklarowany tutaj, a zdefiniowany w pliku .cpp)
- Metodę
setKey(int key)do ustawiania klucza szyfrowania - Metodę
getEncrypted(), która zwraca zaszyfrowaną wiadomość jakostd::string - Metodę
getOriginal(), która zwraca oryginalną wiadomość
Plik nagłówkowy powinien pokazywać wyłącznie publiczny interfejs: nie powinny być tu widoczne żadne szczegóły implementacji szyfrowania.
- Konstruktor przyjmujący
SecureMessage.cpp: Zdefiniuj zagnieżdżoną klasęImpli zaimplementuj wszystkie metody.Klasa
Implpowinna przechowywać oryginalną wiadomość i klucz szyfrowania (domyślnie 0), a także obsługiwać właściwą logikę szyfrowania. Do szyfrowania użyj prostego szyfru Cezara: przesuwaj każdy znak o wartość klucza. Na przykład przy kluczu 3 litera 'a' zmienia się w 'd', a 'z' zawija się i zmienia w 'c'.Szyfrowanie powinno dotyczyć wyłącznie małych liter (a-z), pozostawiając wszystkie pozostałe znaki bez zmian. Pamiętaj, aby zdefiniować tutaj destruktor (nawet jeśli jest domyślny), ponieważ
unique_ptrwymaga kompletnego typuImpl.main.cpp: Wczytaj dwa dane wejściowe:- Ciąg znaków wiadomości (może zawierać spacje)
- Klucz szyfrowania (liczba całkowita)
Utwórz obiekt
SecureMessage, ustaw klucz i wyświetl wyniki:- Wypisz
Original:, a następnie oryginalną wiadomość - Wypisz
Encrypted:, a następnie zaszyfrowaną wiadomość
Na przykład dla danych wejściowych hello world i 3:
Original: hello world
Encrypted: khoor zruogDla danych wejściowych xyz abc i 5:
Original: xyz abc
Encrypted: cde fghZauważ, że plik nagłówkowy nie ujawnia niczego na temat implementacji szyfru Cezara: na tym polega siła Pimpl. Jeśli później zmienisz algorytm szyfrowania, ponownej kompilacji będzie wymagał tylko SecureMessage.cpp, a nie żaden plik dołączający plik nagłówkowy.
Spróbuj swoich sił
#include <iostream>
#include <string>
#include "SecureMessage.h"
int main() {
// Wczytaj wiadomość (może zawierać spacje)
std::string message;
std::getline(std::cin, message);
// Wczytaj klucz szyfrowania
int key;
std::cin >> key;
// TODO: Utwórz obiekt SecureMessage z wiadomością
// TODO: Ustaw klucz szyfrowania
// TODO: Wypisz "Original: ", a następnie oryginalną wiadomość
// TODO: Wypisz "Encrypted: ", a następnie zaszyfrowaną wiadomość
return 0;
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Podstawy programowania obiektowego
Pliki zewnętrzneBudowanie i kompilacja C++Pliki nagłówkowe i pliki źródłowePrzestrzenie nazw i zakresWprowadzenie do programowania obiektowego w C++Klasy a obiektyWskaźnik „this”Metody (funkcje składowe)Atrybuty (składowe danych)Podstawy konstruktorów i destruktorówPowtórzenie – prosty kalkulator4Właściwości klas
Elementy instancji a elementy statyczneGettery i setteryStałe funkcje składoweSłowo kluczowe mutableMetody i zmienne statyczneFunkcje i klasy zaprzyjaźnionePodsumowanie – menedżer konta bankowego7Dziedziczenie
Podstawy dziedziczeniaPoziomy dostępu w dziedziczeniuKolejność wywołań konstruktora i destruktoraNadpisywanie metodFunkcje wirtualne i VTableWielokrotne dziedziczenieDziedziczenie wirtualnePowtórzenie — hierarchia pracowników2Zarządzanie pamięcią
Pamięć stosu a stertyWskaźniki i referencjePamięć dynamiczna (new/delete)Inteligentne wskaźniki w C++RAII w C++Podsumowanie — menedżer tablicy dynamicznej5Hermetyzacja
Specyfikatory dostępu w C++Specyfikatory dostępu — szczegółowoUkrywanie informacjiStruktura a klasaKlasy zagnieżdżone i wewnętrznePodsumowanie — system ewidencji studentów8Polimorfizm
Polimorfizm czasu kompilacji i wykonaniaPrzeciążanie funkcjiFunkcje wirtualne — powtórkaCzysto wirtualne funkcjeKlasy abstrakcyjneProjektowanie interfejsów w C++Rzutowanie dynamiczne i RTTIPodsumowanie — kalkulator kształtów11Zaawansowane koncepcje OOP
Kompozycja a dziedziczenieMixiny z użyciem CRTPIdiom PimplWymazywanie typówKlasy enum i silne typowanieObsługa wyjątków w OOPWłasne hierarchie wyjątków3Konstruktory i destruktory
Konstruktor domyślnyKonstruktor z parametramiKonstruktor kopiującyKonstruktor przenoszącyListy inicjalizacyjne konstruktoraKonstruktory delegująceDestruktor — szczegółowe omówienieZasada trzech / pięciu / zeraPodsumowanie — klasa String6Przeciążanie operatorów
Wprowadzenie do przeciążania operatorówPrzeciążanie operatorów arytmetycznychPrzeciążanie operatorów porównaniaOperatory strumieniowePrzeciążanie operatora przypisaniaPrzeciążanie operatorów [] i ()Operatory konwersji typówPodsumowanie — klasa Matrix9Szablony
Szablony funkcjiSzablony klasSpecjalizacja szablonówSzablony wariadycznePodstawy SFINAE i cech typówPodsumowanie — kontener generycznyPoćwicz samodzielnie: Kompilator C++ online