Zajímavosti z C++: Čistě virtuální destruktor

Od Petr Zemek, 2013-03-24

Možná jste se už během své programátorské praxe dostali do situace, kdy jste potřebovali vaši bázovou třídu udělat čistě virtuální, ale žádná z metod k tomu nebyla vhodná. Jednou z možností, která je nepříliš známá, je využít destruktor. O tom, jak to udělat, je následující příspěvek.

Popis situace

Představte si, že máte bázovou třídu Base, kterou chcete udělat čistě virtuální. To v kontextu jazyka C++ znamená, že tuto třídu nebude možno instanciovat bez toho, aniž byste vytvořili její podtřídu a implementovali všechny čistě virtuální metody, které se nachází v bázové třídě. Příkladem využití je, když chcete v C++ definovat rozhraní (angl. interface), které mají implementovat všechny podtřídy. V C++ se to řeší tak, že se zvolená virtuální metoda (či více metod) označí jako čistě virtuální pomocí = 0. Ty pak musí podtřídy implementovat, jinak je nepůjde instanciovat. To je ukázáno na následujícím příkladu.

class Base {
public:
    virtual ~Base();
 
    virtual void f() = 0;
};
 
Base::~Base() {}

Destruktor byl označen jako virtuální z toho důvodu, aby nám korektně fungovala destrukce v případě, kdy použijeme ukazatel na bázovou třídu, který bude ukazovat na podtřídu. Pokud bychom to neudělali, tak překladačem vygenerovaný destruktor by virtuální nebyl.

Pak si vytvoříme podtřídu, která implementuje čistě virtuální metodu f():

class Derived: public Base {
public:
    virtual void f();
}
 
void Derived::f() {
    // ...
}

Destruktor si psát sami nemusíme, protože když má nadtřída virtuální destruktor, tak je překladačem automaticky vygenerovaný destruktor v podtřídě automaticky virtuální.

Pak si můžeme vesele instanciovat Derived.

Derived d;

Doposud nic nového. Co když ale bázová třída žádnou vhodnou metodu, kterou bychom označili jako čistě virtuální, nemá? Taková situace může nastat, když v bázové třídě sice máte virtuální metody, ale u všech máte implementaci a není potřeba, aby je podtřída přeimplementovávala. Nebo v situaci, kdy vaše bázová třída slouží jako tzv. tagging interface (taktéž nazývaný marker interface), tedy nemá žádné metody a nedává smysl ji samu o sobě instanciovat.

Co s tím?

Využijeme následující dvě vlastnosti jazyka C++.

  • Čistě virtuální destruktor Ač to na první pohled může znít zvláštně, v C++ lze jako čistě virtuální označit i destruktor:
    class Base {
    public:
        virtual ~Base() = 0;
    };
  • Výchozí implementace. V C++ lze i u čistě virtuálních metod uvést výchozí implementaci, kterou mohou podtřídy využít. V našem případě tento fakt aplikujeme na destruktor:
    Base::~Base() {}

Když si nyní vytvoříme podtřídu, můžeme ji instanciovat, aniž bychom museli cokoliv definovat.

class Derived: public Base {};
 
Derived d;

Hurá :).

Na co si dát pozor

Je třeba si dát pozor na dvě věci. První je, že u virtuálního destruktoru nesmíte zapomenout na výchozí implementaci. V C++ totiž platí, že po tom, co se zavolá destruktor nadtřídy, se automaticky zavolá destruktor podtřídy. Pokud by destruktor bázové třídy nebyl implementovaný, dostaneme chybu při linkování:

/tmp/ccWty6GK.o: In function `Derived::~Derived()':
c.cc:(.text._ZN7DerivedD2Ev[_ZN7DerivedD5Ev]+0x1f): undefined reference to `Base::~Base()'

Druhou je, že někoho by mohlo napadnout využít některou z jiných virtuálních metod, které se nachází v nadtřídě, tu udělat čistě virtuální, definovat výchozí implementaci a vyhnout se tak nutnosti definovat jako čistě virtuální destruktor:

class Base {
public:
    virtual ~Base();
 
    virtual void f() = 0;
};
 
void Base::f() {
    // ... výchozí implementace
}
 
class Derived: public Base {};
 
Derived d;

Zde ale narazíte hned při překladu:

error: cannot declare variable ‘d’ to be of abstract type ‘Derived’
note:   because the following virtual functions are pure within ‘Derived’:
note:        virtual void Base::f()

Důvodem je, že i když má čistě virtuální metoda definovanou výchozí implementaci, i přesto je nutné ji implementovat, protože je čistě virtuální. Teď vás asi napadne, jak to, že jsme v případě čistě virtuálního destruktoru nemuseli v podtřídě nic implementovat? Důvodem je, že pokud ve třídě nedefinujeme destruktor, tak jej za nás automaticky vygeneruje překladač :). V tom tkví výhoda použití čistě virtuálního destruktoru.

Závěrem

Cílem tohoto příspěvku bylo poukázat na možnost využití čistě virtuálního destruktoru. Rozhodně byste jej neměli aplikovat ve všech případech, pouze v těch, kdy opravdu není žádná z metod vhodná k tomu, aby byla označena jako čistě virtuální. Dejte si však pozor na to, že toto může značit chybu v návrhu.

Obsah tohoto pole je soukromý a nebude veřejně zobrazen.

Filtrované HTML (využíváno)

  • Povolené HTML značky: <a href hreflang> <em> <strong> <cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <table>
  • Zvýraznění syntaxe kódu lze povolit přes následující značky: <code>, <blockcode>, <bash>, <c>, <cpp>, <haskell>, <html>, <java>, <javascript>, <latex>, <perl>, <php>, <python>, <ruby>, <rust>, <sql>, <text>, <vim>, <xml>, <yaml>.
  • Řádky a odstavce se zalomí automaticky.
  • Webové a e-mailové adresy jsou automaticky převedeny na odkazy.
CAPTCHA
7 + 10 =
Vyřešte tento jednoduchý matematický příklad a vložte výsledek. Např. pro 1+3 vložte 4.
Nějak se mi tady rozmohl spam, takže poprosím o ověření.