Pravidlo tří (před C++11), resp. pravidlo pěti (od C++11) jsou doporučení pro vytváření kódu v programovacím jazyce C++ popisující používání implicitních (tj. překladačem definovaných) členských funkcí třídy. Pokud má být program odolný proti chybám při použití výjimek a má využívat formalizující pravidla správy prostředků. Pravidlo říká, že pokud programátor explicitně nadefinoval některou ze speciálních členských funkcí, měl by nejspíše explicitně nadefinovat všechny.

Pravidlo tří Pravidlo tří (známé také jako zákon velkých tři nebo velká trojka) je praktické pravidlo v C++ (před verzí C++11), které říká, že pokud třída definuje některou z následujících tří speciálních funkcí, pak pravděpodobně musí explicitně definovat všechny tři:

Destruktor Kopírovací konstruktor Kopírovací operátor přiřazení

Tyto tři funkce jsou speciální členské funkce. Pokud je některá z těchto funkcí použita, aniž byla deklarována programátorem, bude implicitně implementována překladačem s touto implicitní sémantikou:

Destruktor vyvolá destruktory všech členů objektu, které jsou typu třída Kopírovací konstruktor vytvoří všechny členy objektu z odpovídajících členů argumentu kopírovacího konstruktoru, přičemž vyvolá kopírovací konstruktory členů objektu, které jsou typu třída, a provede prosté přiřazení všech datových členů, které typu třída nejsou (například int nebo ukazatel) Kopírovací operátor přiřazení – přiřadí všechny členy objektu z odpovídajících členů argumentu přiřazovacího operátoru, přičemž vyvolá kopírovací přiřazovací operátory pro členy objektu, které jsou typu třída, a provede prosté přiřazení všech datových členů, které typu třída nejsou (například int nebo ukazatel).

Pravidlo tří říká, že pokud některou z těchto funkcí musel nadefinovat programátor, znamená to, že překladačem vygenerovaná verze v tomto případě nevyhovuje potřebám třídy a pravděpodobně nebude vyhovovat ani v ostatních případech. Termín „Pravidlo tří“ poprvé použil Marshall Cline v roce 1991.

Výjimkou z tohoto pravidla je situace, kdy je třída navržena takovým způsobem, že pro všechny její (netriviální) členy je použita inicializace prostřednictvím získání prostředku (RAII), destruktor může zůstat nedefinovaný (tzv. „zákon velkých dvou“). Hotovým příkladem tohoto přístupu je použití smart pointerů místo obyčejných ukazatelů.

Protože implicitně generované konstruktory a operátory přiřazení jednoduše kopírují všechny datové členy třídy („mělká kopie“), je třeba definovat explicitní kopírovací konstruktory a kopírovací operátory přiřazení pro třídy, které zapouzdřují složité datové struktury nebo mají externí odkazy, jako jsou ukazatele, pokud je třeba zkopírovat objekty, na které ukazují členy třídy. Pokud je skutečně požadované implicitní chování („mělká kopie“), pak explicitní definice, i když je redundantní, bude představovat „samodokumentující kód“, který naznačuje, že se jednalo o záměr, nikoli o opomenutí. Moderní C++ obsahuje syntaxi pro výslovné určení, že se má použít implicitní funkce, aniž by bylo nutné psát tělo funkce.

Pravidlo pěti Od verze C++11 je třeba pravidlo tří rozšířil na pravidlo pěti (známé také jako „pravidlo velkých pěti“) protože C++11 implementuje přesunovou sémantiku, která umožňuje, aby nový objekt převzal data od původního objektu. Následující příklad ukazuje nové přesouvací členy: přesouvací konstruktor a přesouvací operátor přiřazení. Pro pravidlo pěti tak máme následující speciální členské funkce:

destruktor kopírovací konstruktor kopírovací přiřazení operátor přesouvací konstruktor přesouvací operátor přiřazení Existují situace, kdy třídy potřebují destruktory, ale nemohou citlivě implementovat kopírovací a přesouvací konstruktory a kopírovací a přesouvací operátory přiřazení. Může to například nastat, pokud bázová třída nepodporuje tyto další velké čtyři členy, ale konstruktor odvozené třídy alokuje paměť pro vlastní použití. V C++11 lze jednoduše explicitně definovat pět členů jako implicitních.

Příklady Prostředky dostupné pomocí manipulátorů Pokud datový prvek reprezentuje prostředek (např. soubor, TCP spojení nebo připojení k databázi) prostřednictvím manipulátoru, kompilátor o tomto významu zpravidla neví. Manipulátor může být například hodnota int. Aby byl reprezentovaný prostředek v destruktoru třídy uzavřen nebo uvolněn, musí mít třída uživatelsky definovaný destruktor, ve kterém bude prostředek explicitně uzavřen nebo uvolněn např. pomocí systémového volání.

Podobně musí kopírovací konstruktor a přiřazovací operátor provést další akce kromě zkopírování manipulátoru, aby k danému prostředku mohl přistupovat bez konfliktů jak původní objekt tak jeho kopie. Pokud k danému prostředku nemá nebo nemůže přistupovat více objektů, je třeba obě kopírovací funkce explicitně odstranit, čímž se zabrání kopírování objektu tohoto typu:

class Datovy_soubor { public: Datovy_soubor(const char* filename) : file(fopen(filename, "rb")) { /* Zpracování chyb atd. */ }

// Pravidlo tří (pravidlo pěti): Datovy_soubor(const Datovy_soubor&) = delete; // Znemožnit kopírování ~Datovy_soubor() { fclose(file); } void operator=(const Datovy_soubor&) = delete; // Znemožnit kopírování

// další členské funkce // ...

private: FILE* file;

Prostředky dostupné pomocí „holého“ ukazatele Podobná situace nastává, když se na prostředek odkazuje pomocí „holého ukazatele“. Funkce generované kompilátorem sice zkopírují ukazatel, ale ne prostředek, na který odkazuje. Tomu se také říká mělká kopie. V důsledku toho původní objekt a jeho kopie prostředek sdílejí. Přitom není jasné, kterému z objektů sdílený prostředek „patří“, tedy kdo je zodpovědný za uvolnění prostředku.

Třída proto potřebuje uživatelsky definované konstruktor kopírování a operátor přiřazení. V nich musí být buď explicitně duplikován odkazovaný prostředek (hluboká kopie), nebo musí být přístup upraven jiným způsobem; případně je i zde jediným smysluplným řešením zcela zakázat kopírování objektů této třídy explicitním odstraněním těchto kopírovacích funkcí.

Moderní kompilátory nabízejí možnost vydat varování, pokud je definována třída, která obsahuje „holé ukazatele“ jako datové prvky, ale nesplňuje pravidlo tří.

Jinou možností je použití inteligentních ukazatelů (smart pointers), které referencovaný prostředek definovaným způsobem zapouzdřují a přitom jasně upravují přístup k potenciálně sdílenému prostředku a dobu jeho životnosti. Standardní knihovna C++ k tomu od verze C++11 poskytuje vlastní třídy inteligentních ukazatelů:

Druh smart pointeru Význam std::unique_ptr<t> Prostředek není možné implicitně kopírovat. Při pokusu o kopírování objektu, který obsahuje datové prvky typu unique_ptr, ale nemá žádné uživatelsky definované kopírovací funkce, překladač vydá chybu. std::shared_ptr<t> Odkazovaný prostředek stal je sdílen původním a cílovým objektem. Počet kopií je zaznamenán pomocí čítače, takže prostředek bude uvolněn při zániku poslední kopie. std::weak_ptr<t>

unique_ptr a shared_ptr navíc obsahují volitelnou funkci "deleter" pro případ, že odkazovaný prostředek nelze uvolnit pouhým použitím delete.

Odkazy Reference

Související články C++ třídy Třída (počítač programování)

Kategorie:C++ Kategorie:Softwarové inženýrství