Idiome und Design Patterns bieten beide bewährte Lösungen für wiederkehrende Probleme in der Softwareentwicklung. Sie unterscheiden sich jedoch hinsichtlich Umfang, Granularität und Formalisierungsgrad:
- Umfang:
- Idiome: Idiome sind kleine, sprachspezifische Programmiertechniken oder Muster, die konkrete Herausforderungen innerhalb einer bestimmten Programmiersprache lösen. Häufig nutzen sie gezielt Spracheigenschaften oder Konventionen, um ein gewünschtes Ergebnis effizient zu erreichen.
- Design Patterns: Design Patterns sind übergeordnete, sprachunabhängige Architekturlösungen für häufige Entwurfsprobleme in der Softwareentwicklung. Sie stellen allgemein wiederverwendbare Vorlagen bereit und fördern bewährte Praktiken beim Softwaredesign.
- Granularität:
- Idiome: Idiome sind meist feingranularer und konzentrieren sich auf bestimmte Sprachkonstrukte oder Techniken innerhalb einer einzelnen Programmiersprache. Oft werden Spracheigenschaften oder Syntax gezielt eingesetzt, um ein bestimmtes Ziel zu erreichen.
- Design Patterns: Design Patterns sind umfassender und behandeln allgemeinere Entwurfskonzepte sowie Beziehungen zwischen Komponenten eines Softwaresystems. Sie liefern Vorlagen zur Organisation und Strukturierung von Code auf einer höheren Abstraktionsebene.
- Formalisierungsgrad:
- Idiome: Idiome sind üblicherweise informell und verbreiten sich innerhalb einer Entwicklergemeinschaft durch Erfahrung, Code Reviews oder Fachliteratur. Sie besitzen nicht immer einen offiziellen Namen oder eine formale Dokumentation.
- Design Patterns: Design Patterns sind stärker formalisiert und gut dokumentierte Lösungen für häufige Entwurfsprobleme. Sie besitzen meist etablierte Namen, Beschreibungen und Implementierungsrichtlinien, wie sie beispielsweise im Gang-of-Four-Buch (GoF) „Design Patterns: Elements of Reusable Object-Oriented Software“ beschrieben werden.
In diesem Beitrag sehen wir uns die folgenden verbreiteten C++-Idiome an:
- 1 - RAII
- 2 - Pimpl
- 3 - Curiously Recurring Template Pattern (CRTP)
- 4 - Copy-and-swap
- 5 - Type Erasure
- 6 - Non-Virtual Interface (NVI)
- 7 - SFINAE (Substitution Failure Is Not An Error)
Im ersten Teil konzentrieren wir uns auf die Idiome RAII und PImpl:
RAII(Resource Acquisition Is Initialization)
Eines der am häufigsten verwendeten Idiome in C++ ist RAII (Resource Acquisition Is Initialization). RAII ist eine leistungsfähige und weit verbreitete Technik, um Ressourcen wie Speicher, Datei-Handles, Netzwerkverbindungen und Sperren deterministisch und ausnahmesicher zu verwalten. Dabei wird die Lebensdauer einer Ressource an die Lebensdauer eines Objekts gekoppelt, sodass Ressourcen zuverlässig angefordert und wieder freigegeben werden.
So funktioniert RAII und in diesen Bereichen wird es häufig eingesetzt:
1-Speicherverwaltung: RAII wird in C++ intensiv zur Verwaltung dynamisch reservierten Speichers über Zeiger eingesetzt, insbesondere mit std::unique_ptr and std::shared_ptr aus der C++-Standardbibliothek. Diese Smart-Pointer-Klassen sorgen dafür, dass Speicher automatisch freigegeben wird, sobald das Objekt mit dem Zeiger seinen Gültigkeitsbereich verlässt.
{
std::shared_ptr<int> ptr(new int); // Resource acquired
// Use ptr
} // Resource released automatically when ptr goes out of scope
2-Dateiverwaltung: RAII wird zur Verwaltung von Dateiressourcen eingesetzt und stellt sicher, dass Dateien nach der Verwendung ordnungsgemäß geschlossen werden – auch bei Exceptions oder vorzeitigen Rückgaben.
{
std::ifstream file("example.txt"); // Resource acquired
// Read from file
} // Resource released automatically when file goes out of scope
3-Sperrmechanismen: RAII wird zur Verwaltung von Locks eingesetzt, damit kritische Codeabschnitte korrekt synchronisiert und Sperren zuverlässig freigegeben werden, sobald sie nicht mehr benötigt werden.
{
std::ifstream file("example.txt"); // Resource acquired
// Read from file
} // Resource released automatically when file goes out of scope
4-Ressourcenverwaltung in eigenen Klassen: Entwickler implementieren RAII häufig in eigenen Klassen, um Ressourcen wie Datenbankverbindungen, Netzwerk-Sockets oder GPU-Ressourcen zu verwalten.
RAII erhöht die Ressourcensicherheit, verringert das Risiko von Ressourcenlecks und Dangling Pointers und vereinfacht die Verwaltung, indem Ressourcen an Objektlebenszeiten gebunden werden. Das Idiom gilt als einer der Grundpfeiler moderner C++-Programmierung und wird sowohl in der Sprache als auch in ihrer Standardbibliothek umfassend eingesetzt.
Pimpl
Das Pimpl-Idiom („Pointer to Implementation“) ist eine C++-Entwurfstechnik, mit der die Implementierungsdetails einer Klasse vor ihren Nutzern verborgen werden. Es verbessert die Kapselung, reduziert Abhängigkeiten beim Kompilieren und minimiert notwendige Neukompilierungen, wenn sich Implementierungsdetails ändern.
So funktioniert das Pimpl-Idiom:
- Trennung von Schnittstelle und Implementierung: Beim Pimpl-Idiom wird die öffentliche Schnittstelle einer Klasse in der Header-Datei definiert, während private Implementierungsdetails in einer separaten Implementierungsdatei gekapselt werden. Dadurch interagiert Client-Code ausschließlich mit der öffentlichen Schnittstelle, während die Komplexität der Implementierung verborgen bleibt.
- Zeiger auf die Implementierung: Statt die Implementierungsdetails direkt in die Klassendeklaration aufzunehmen, enthält die Klasse einen Zeiger auf eine opake, also vorwärtsdeklarierte Implementierungsklasse. Dieser Zeiger wird üblicherweise als privates Klassenmitglied deklariert.
- Vorwärtsdeklaration: Da die Implementierungsdetails vor den Nutzern der Klasse verborgen sind, wird in der Header-Datei lediglich die Deklaration der Implementierungsklasse benötigt. Dies wird durch eine Vorwärtsdeklaration erreicht (
class Impl;), sodass die Implementierungsdetails nicht in den Header aufgenommen werden müssen. - Weniger Abhängigkeiten beim Kompilieren: Durch die Trennung von Schnittstelle und Implementierung sowie den Einsatz von Vorwärtsdeklarationen erfordern Änderungen an Implementierungsdetails – etwa das Hinzufügen, Entfernen oder Ändern privater Member – keine Neukompilierung des Codes, der lediglich die Klassenschnittstelle verwendet. Besonders bei großen Codebasen kann dies die Build-Zeiten deutlich reduzieren.
- Dynamische Speicherallokation: Üblicherweise wird die Implementierungsklasse dynamisch auf dem Heap mit
new, während der Zeiger darauf von der öffentlichen Klasse verwaltet wird. Dies ermöglicht dynamischen Polymorphismus und den Einsatz der Design Patterns „Bridge“ und „Strategy“.
Hier ist ein vereinfachtes Beispiel für das Pimpl-Idiom:
// MyClass.h (Header file)
#pragma once
class MyClass {
public:
MyClass();
~MyClass();
void doSomething();
private:
class Impl; // Forward declaration
Impl* pImpl; // Pointer to implementation
};
// MyClass.cpp (Implementation file)
#include "MyClass.h"
class MyClass::Impl {
public:
void doInternalWork() {
// Implementation details...
}
};
MyClass::MyClass() : pImpl(new Impl()) {}
MyClass::~MyClass() {
delete pImpl;
}
void MyClass::doSomething() {
pImpl->doInternalWork();
}
Durch den Einsatz des Pimpl-Idioms werden die Implementierungsdetails von MyClass vor den Nutzern der Klasse verborgen. Änderungen an der Implementierung können vorgenommen werden, ohne Client-Code zu beeinflussen oder abhängige Dateien neu kompilieren zu müssen. Das verbessert Modularität, Wartbarkeit und Build-Zeiten in C++-Projekten.
