25.4 — Wirtualne destruktory, wirtualne przypisanie i przesłonięcie wirtualizacja

Wirtualne destruktory

Chociaż C++ zapewnia domyślny destruktor dla twoich klas, jeśli nie podasz go samodzielnie, czasami może się zdarzyć, że będziesz chciał udostępnić własny destruktor (szczególnie jeśli klasa musi zwolnić pamięć). Powinieneś zawsze uczynić swoje destruktory wirtualnymi, jeśli masz do czynienia z dziedziczeniem. Rozważmy następujący przykład:

#include <iostream>
class Base
{
public:
    ~Base() // note: not virtual
    {
        std::cout << "Calling ~Base()\n";
    }
};

class Derived: public Base
{
private:
    int* m_array {};

public:
    Derived(int length)
      : m_array{ new int[length] }
    {
    }

    ~Derived() // note: not virtual (your compiler may warn you about this)
    {
        std::cout << "Calling ~Derived()\n";
        delete[] m_array;
    }
};

int main()
{
    Derived* derived { new Derived(5) };
    Base* base { derived };

    delete base;

    return 0;
}

Uwaga: Jeśli skompilujesz powyższy przykład, kompilator może ostrzec Cię o niewirtualnym destruktorze (co jest zamierzone w tym przykładzie). Aby kontynuować, może być konieczne wyłączenie flagi kompilatora, która traktuje ostrzeżenia jako błędy.

Ponieważ base jest wskaźnikiem Base, po usunięciu base program sprawdza, czy destruktor Base jest wirtualny. Tak nie jest, więc zakłada, że ​​wystarczy wywołać destruktor Base. Widzimy to po tym, że powyższy przykład wypisuje:

Calling ~Base()

Ale naprawdę chcemy, aby funkcja usuwania wywołała destruktor Derived (który z kolei wywoła destruktor Base), w przeciwnym razie m_array nie zostanie usunięty. Robimy to poprzez uczynienie destruktora Base wirtualnym:

#include <iostream>
class Base
{
public:
    virtual ~Base() // note: virtual
    {
        std::cout << "Calling ~Base()\n";
    }
};

class Derived: public Base
{
private:
    int* m_array {};

public:
    Derived(int length)
      : m_array{ new int[length] }
    {
    }

    virtual ~Derived() // note: virtual
    {
        std::cout << "Calling ~Derived()\n";
        delete[] m_array;
    }
};

int main()
{
    Derived* derived { new Derived(5) };
    Base* base { derived };

    delete base;

    return 0;
}

Teraz ten program generuje następujący wynik:

Calling ~Derived()
Calling ~Base()

Reguła

Za każdym razem, gdy masz do czynienia z dziedziczeniem, powinieneś uczynić dowolne jawne destruktory wirtualnymi.

Podobnie jak w przypadku normalnych wirtualnych funkcji składowych, jeśli funkcja klasy bazowej jest wirtualna, wszystkie pochodne przesłonięcia będą uważane za wirtualne, niezależnie od tego, czy zostały określone jako takie. Nie jest konieczne tworzenie pustego destruktora klasy pochodnej tylko po to, aby oznaczyć go jako wirtualny.

Zauważ, że jeśli chcesz, aby Twoja klasa bazowa miała wirtualny destruktor, który w innym przypadku byłby pusty, możesz zdefiniować swój destruktor w ten sposób:

    virtual ~Base() = default; // generate a virtual default destructor

Przypisanie wirtualne

Możliwe jest uczynienie operatora przypisania wirtualnym. Jednak w przeciwieństwie do destruktora, w którym wirtualizacja jest zawsze dobrym pomysłem, wirtualizacja operatora przypisania tak naprawdę otwiera worek pełen robaków i pozwala na zapoznanie się z niektórymi zaawansowanymi tematami wykraczającymi poza zakres tego samouczka. W związku z tym, dla uproszczenia, zalecamy na razie pozostawienie zadań niewirtualnych.

Ignorowanie wirtualizacji

Bardzo rzadko możesz chcieć zignorować wirtualizację funkcji. Rozważmy na przykład następujący kod:

#include <string_view>
class Base
{
public:
    virtual ~Base() = default;
    virtual std::string_view getName() const { return "Base"; }
};

class Derived: public Base
{
public:
    virtual std::string_view getName() const { return "Derived"; }
};

Może się zdarzyć, że wskaźnik Base do obiektu Derived będzie wywoływał Base::getName() zamiast Derived::getName(). Aby to zrobić, po prostu użyj operatora rozpoznawania zakresu:

#include <iostream>
int main()
{
    Derived derived {};
    const Base& base { derived };

    // Calls Base::getName() instead of the virtualized Derived::getName()
    std::cout << base.Base::getName() << '\n';

    return 0;
}

Prawdopodobnie nie będziesz go używać zbyt często, ale dobrze wiedzieć, że jest to chociaż możliwe.

Czy powinniśmy uczynić wszystkie destruktory wirtualnymi?

To częste pytanie zadawane przez nowych programistów. Jak zauważono w najwyższym przykładzie, jeśli destruktor klasy bazowej nie jest oznaczony jako wirtualny, wówczas w programie istnieje ryzyko wycieku pamięci, jeśli programista później usunie wskaźnik klasy bazowej wskazujący na obiekt pochodny. Jednym ze sposobów uniknięcia tego jest oznaczenie wszystkich destruktorów jako wirtualnych. Ale czy powinieneś?

Łatwo jest powiedzieć tak, dzięki czemu możesz później użyć dowolnej klasy jako klasy bazowej - ale wiąże się to z pogorszeniem wydajności (wirtualny wskaźnik dodany do każdej instancji twojej klasy). Musisz więc zrównoważyć ten koszt i swoje intencje.

Sugerujemy, co następuje: Jeśli klasa nie jest wyraźnie zaprojektowana jako klasa bazowa, ogólnie lepiej jest nie mieć wirtualnych elementów ani wirtualnego destruktora. Klasy można nadal używać poprzez kompozycję. Jeśli klasa została zaprojektowana do użycia jako klasa bazowa i/lub ma jakiekolwiek funkcje wirtualne, powinna zawsze mieć wirtualny destruktor.

Jeśli zostanie podjęta decyzja, że ​​klasa nie będzie dziedziczona, następnym pytaniem jest, czy można to wymusić.

Konwencjonalna mądrość (jak początkowo przedstawił Herb Sutter, wysoko ceniony guru C++) sugeruje unikanie sytuacji wycieku pamięci destruktora niewirtualnego w następujący sposób: „Destruktor klasy bazowej powinien być albo publiczny i wirtualny, albo chroniony i niewirtualny”. Klasy bazowej z chronionym destruktorem nie można usunąć za pomocą wskaźnika klasy bazowej, co uniemożliwia usunięcie obiektu klasy pochodnej poprzez wskaźnik klasy bazowej.

Niestety uniemożliwia to również dowolnego użycie destruktora klasy bazowej przez społeczeństwo. Oznacza to:

  • Nie powinniśmy dynamicznie przydzielać obiektów klasy bazowej, ponieważ nie mamy konwencjonalnego sposobu na ich usunięcie (istnieją niekonwencjonalne obejścia, ale fuj).
  • Nie możemy nawet statycznie przydzielać obiektów klasy bazowej, ponieważ destruktor nie jest dostępny, gdy wykraczają one poza zakres.

Innymi słowy, używając tej metody, aby klasa pochodna była bezpieczna, musimy zapewnić bezpieczeństwo klasa bazowa jest praktycznie bezużyteczna sama w sobie.

Teraz, gdy specyfikator final został wprowadzony do języka, nasze zalecenia są następujące:

  • Jeśli chcesz, aby Twoja klasa była dziedziczona, upewnij się, że Twój destruktor jest wirtualny i publiczny.
  • Jeśli nie chcesz, aby Twoja klasa była dziedziczona, oznacz ją jako ostateczną. Zapobiegnie to przede wszystkim dziedziczeniu z niej przez inne klasy, bez nakładania jakichkolwiek innych ograniczeń użytkowania na samą klasę.
guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
200 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze