RAII als Muster
Teil des Abschnitts Objektorientierte Programmierung der C++-Journey von Coddy. Lektion 102 von 104.
RAII (Resource Acquisition Is Initialization) ist mehr als nur ein C++-Idiom – es ist ein leistungsfähiges Entwurfsmuster, das die Ressourcenverwaltung an die Lebensdauer von Objekten bindet. Du hast RAII bereits bei Smart Pointern gesehen, aber das Muster gilt für jede Ressource: Dateihandles, Netzwerkverbindungen, Mutexe oder Datenbanktransaktionen.
Die Grundidee ist einfach: Ressourcen im Konstruktor erwerben und sie im Destruktor freigeben. Da C++ garantiert, dass Destruktoren ausgeführt werden, wenn Objekte ihren Gültigkeitsbereich verlassen, erfolgt die Bereinigung automatisch – selbst wenn Ausnahmen auftreten:
#include <iostream>
#include <fstream>
class FileGuard {
std::ofstream file;
public:
FileGuard(const std::string& filename) : file(filename) {
if (!file.is_open()) {
std::cout << "Failed to open file\n";
}
}
void write(const std::string& text) {
if (file.is_open()) file << text;
}
~FileGuard() {
if (file.is_open()) {
file.close();
std::cout << "File closed automatically\n";
}
}
};
int main() {
{
FileGuard guard("output.txt");
guard.write("Hello RAII");
} // Destruktor hier aufgerufen – Datei geschlossen
std::cout << "After scope\n";
}RAII spielt seine Stärken bei der Verwaltung von Sperren in nebenläufigem Code aus. std::lock_guard der Standardbibliothek folgt diesem Muster – es sperrt einen Mutex bei der Konstruktion und gibt ihn bei der Destruktion frei, wodurch Deadlocks aufgrund vergessener Entsperrungen verhindert werden.
Denke bei der Implementierung von RAII-Klassen daran, entweder Kopier-/Verschiebeoperationen zu löschen oder sie ordnungsgemäß zu implementieren (Rule of Five), um die Duplizierung von Ressourcen oder Probleme durch doppelte Freigabe zu verhindern. RAII verwandelt die fehleranfällige manuelle Ressourcenverwaltung in eine sichere, automatische Bereinigung.
Aufgabe
EinfachWir erstellen einen Connection Pool Manager mit RAII, um Datenbankverbindungen sicher zu verwalten. In realen Anwendungen sind Datenbankverbindungen Ressourcen, deren acquire und release ordnungsgemäß erfolgen müssen. Du erstellst einen RAII-Wrapper, der garantiert, dass connections immer an den Pool zurückgegeben werden, selbst wenn Ausnahmen auftreten oder die Codepfade kompliziert werden.
Du organisierst deinen Code auf drei Dateien:
ConnectionPool.h: Erstelle einen einfachen Connection Pool, der eine begrenzte Anzahl von connections verwaltet.Deine
ConnectionPool-Klasse sollte verfolgen, wie viele connections verfügbar sind (beginnend mit einer an den constructor übergebenen capacity). Implementiere:acquire(): Wenn eine connection verfügbar ist, verringere den count und gibConnection acquired (X available)aus, wobei X der verbleibende count ist; gibtruezurück, wenn der Vorgang erfolgreich war, undfalse, wenn keine connections verfügbar sindrelease(): Erhöhe den count der verfügbaren connections und gibConnection released (X available)ausavailable(): Gibt die aktuelle Anzahl verfügbarer connections zurück
ConnectionGuard.h: Erstelle den RAII-Wrapper, der eine einzelne connection sicher verwaltet.Deine
ConnectionGuard-Klasse verkörpert das RAII-Muster. Sie sollte:- In ihrem constructor eine Referenz auf einen
ConnectionPoolübernehmen und versuchen, eine connection zu erwerben - Speichern, ob der Erwerb erfolgreich war
- Eine
isConnected()-Methode bereitstellen, um zu prüfen, ob der Guard eine gültige connection hält - Die connection im Destructor automatisch an den Pool zurückgeben (nur wenn eine erworben wurde)
- Den copy constructor und die copy assignment löschen, um die Duplizierung von Ressourcen zu verhindern (Berücksichtigung der Rule of Five)
Wenn der Destructor ausgeführt wird und eine connection gehalten wurde, gib
Guard releasing connectionaus, bevor release für den Pool aufgerufen wird.- In ihrem constructor eine Referenz auf einen
main.cpp: Veranschauliche die automatische Bereinigung durch RAII mithilfe von Gültigkeitsbereichen.Lies zwei Eingaben ein:
- Pool capacity (Ganzzahl)
- Anzahl der anzufordernden connections (Ganzzahl)
Erstelle einen
ConnectionPoolmit der angegebenen capacity. Erzeuge dann innerhalb eines verschachtelten Gültigkeitsbereichs (mit using curly braces) die angeforderte Anzahl vonConnectionGuard-Objekten, die in einem Vektor gespeichert werden. Gib für jeden Guard aus, ob die Verbindung erfolgreich hergestellt wurde:- Wenn verbunden:
Guard N: Connected - Wenn nicht verbunden:
Guard N: Failed to connect
(wobei N bei 1 beginnt)
Nachdem der Gültigkeitsbereich endet (die Guards werden zerstört), gib
After scope: X connections availableaus, wobei der endgültige Zustand des Pools angezeigt wird.
Beispielsweise mit den Eingaben 2 und 3:
Connection acquired (1 available)
Guard 1: Connected
Connection acquired (0 available)
Guard 2: Connected
Guard 3: Failed to connect
Guard releasing connection
Connection released (1 available)
Guard releasing connection
Connection released (2 available)
After scope: 2 connections availableMit den Eingaben 3 und 2:
Connection acquired (2 available)
Guard 1: Connected
Connection acquired (1 available)
Guard 2: Connected
Guard releasing connection
Connection released (2 available)
Guard releasing connection
Connection released (3 available)
After scope: 3 connections availableBeachte, wie die connections automatisch freigegeben werden, wenn die Guards ihren Gültigkeitsbereich verlassen. Du rufst release in deinem main-Code niemals explizit auf. Die Destructors werden in umgekehrter Reihenfolge der Erstellung ausgeführt (der letzte Guard wird zuerst zerstört), und jede erworbene connection wird garantiert zurückgegeben. Das ist die Stärke von RAII: Die Ressourcenbereinigung erfolgt automatisch und zuverlässig, unabhängig davon, wie der Gültigkeitsbereich verlassen wird.
Probier es selbst
#include <iostream>
#include <vector>
#include "ConnectionPool.h"
#include "ConnectionGuard.h"
using namespace std;
int main() {
// Eingaben lesen
int capacity;
int numConnections;
cin >> capacity;
cin >> numConnections;
// TODO: Erstelle einen ConnectionPool mit der gegebenen Kapazität
// TODO: Erstelle einen verschachtelten Gültigkeitsbereich mit geschweiften Klammern
{
// TODO: Erstelle einen vector zum Speichern von ConnectionGuard-Objekten
// Hinweis: Du musst Zeiger oder Smart Pointer verwenden, da ConnectionGuard
// einen gelöschten Copy-Konstruktor hat
// TODO: Schleife zum Erstellen von numConnections Guards
// Für jeden Guard drucke entweder:
// "Guard N: Connected" or "Guard N: Failed to connect"
// wobei N bei 1 beginnt
}
// Guards werden hier zerstört, wenn der Gültigkeitsbereich endet
// TODO: Print "After scope: X connections available"
return 0;
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP
Externe DateienC++ Build & KompilierungHeader-Dateien & QuelldateienNamespaces & GültigkeitsbereichEinführung in OOP mit C++Klassen vs. ObjekteDer „this“-ZeigerMethoden (Member-Funktionen)Attribute (Datenmember)Grundlagen zu Ctors & DtorsRückblick – Einfacher Taschenrechner4Klassenattribute
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen und -KlassenZusammenfassung – Bankkontoverwaltung7Vererbung
Grundlagen der VererbungZugriffsebenen bei der VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick – Mitarbeiterhierarchie2Speicherverwaltung
Stack- vs. Heap-SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Intelligente Zeiger in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsbezeichner in C++Zugriffsbezeichner im DetailInformationsverbergungStruct vs. KlasseVerschachtelte und innere KlassenWiederholung – Studierendenverwaltungssystem8Polymorphie
Polymorphie zur Compilezeit vs. LaufzeitFunktionsüberladungVirtuelle Funktionen erneut betrachtetRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIRückblick – Shape-Calculator11Fortgeschrittene OOP-Konzepte
Komposition vs. VererbungMixins mittels CRTPPimpl-IdiomTyplöschungEnum-Klassen & starke TypisierungAusnahmebehandlung in OOPBenutzerdefinierte Ausnahmehierarchien14Entwurfsmuster Teil 2
BefehlsmusterAdaptermusterDekorierermusterTemplate-Method-MusterZustandsmusterKompositmusterRAII als Muster3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorMove-KonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktor im DetailDrei-/Fünf-/Null-RegelZusammenfassung – String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladung der Operatoren [] und ()TypkonvertierungsoperatorenWiederholung – Matrix-Klasse9Templates
FunktionstemplatesKlassen-TemplatesTemplate-SpezialisierungVariadische TemplatesGrundlagen zu SFINAE und Type TraitsRückblick – Generischer ContainerÜbe selbstständig: Online-C++-Compiler