Sobes.tech
Middle+
303

Dlaczego wektor będzie źle działał, jeśli od niego dziedziczymy?

Odpowiedź od AI

sobes.tech AI

Dziedziczenie po std::vector nie jest zalecane ze względu na brak wirtualnych destruktorów i innych funkcji wirtualnych, co narusza zasadę podstawiania Liskov i prowadzi do problemów przy używaniu polimorficznym.

Przykłady problemów:

  1. Problem cięcia (slicing): Przekazywanie obiektu klasy pochodnej przez wartość lub referencję do klasy bazowej std::vector spowoduje utratę danych i zachowania specyficzne dla klasy pochodnej.

    #include <vector>
    #include <iostream>
    
    class MyVector : public std::vector<int> {
    public:
        int my_data = 100;
        // Brak wirtualnego destruktora
        ~MyVector() {
            std::cout << "Destruktor MyVector" << std::endl;
        }
    };
    
    void process_vector(std::vector<int> vec) {
        // Przekazywanie przez wartość - slicing
        // Destruktor MyVector nie zostanie wywołany
        std::cout << "Przetwarzanie std::vector" << std::endl;
    }
    
    int main() {
        MyVector mv;
        mv.push_back(1);
        process_vector(mv); // Slicing obiektu MyVector
        return 0;
    } // Tutaj zostanie wywołany tylko destruktor std::vector
    
  2. Brak wirtualnego destruktora: Jeśli usuwasz obiekt klasy pochodnej przez wskaźnik do klasy bazowej std::vector, destruktor klasy pochodnej nie zostanie wywołany, co może prowadzić do wycieków zasobów.

    #include <vector>
    #include <iostream>
    #include <memory> // dla std::unique_ptr
    
    class DerivedVector : public std::vector<int> {
    public:
        int* resource;
        DerivedVector() : std::vector<int>(), resource(new int) {
            std::cout << "Konstruktor DerivedVector" << std::endl;
        }
        // Brak wirtualnego destruktora
        ~DerivedVector() {
            std::cout << "Destruktor DerivedVector" << std::endl;
            delete resource; // Może nie zostać wywołany
        }
    };
    
    int main() {
        // Usuwanie przez wskaźnik do klasy bazowej
        std::vector<int>* base_ptr = new DerivedVector();
        // Podczas delete base_ptr, zostanie wywołany tylko destruktor std::vector,
        // destruktor DerivedVector nie zostanie wywołany, co powoduje wyciek resource
        delete base_ptr;
    
        // Przykład z std::unique_ptr
        // std::unique_ptr<std::vector<int>> up = std::make_unique<DerivedVector>();
        // up->push_back(5);
        // Po wyjściu ze scope, unique_ptr wywoła delete na wskaźniku surowym base_ptr.
        // To jest równoważne z delete base_ptr; i spowoduje ten sam problem, jeśli vector nie ma wirtualnego destruktora.
    
        return 0;
    } // Pamięć przydzielona dla resource nie zostanie zwolniona
    
  3. Konstruktory: Zachowanie konstruktorów std::vector (np. kopiowania, przenoszenia) może nie odpowiadać oczekiwaniom dla klasy pochodnej, jeśli dodaje ona własny stan lub logikę.

Zamiast dziedziczyć po std::vector, najlepszymi podejściami są:

  • Kompozycja: Używać std::vector jako członka klasy. Pozwala to kontrolować interfejs i zachowanie nowej klasy, korzystając z std::vector wewnątrz.
    #include <vector>
    #include <iostream>
    
    class MyContainer {
    private:
        std::vector<int> data;
        int my_extra_data = 100;
    public:
        void add(int val) {
            data.push_back(val);
        }
        const std::vector<int>& get_data() const { // Udostępniamy dostęp do wektora w razie potrzeby
            return data;
        }
        // Destruktor MyContainer poprawnie wywoła destruktor data
        ~MyContainer() {
             std::cout << "Destruktor MyContainer" << std::endl;
        }
    };
    
    int main() {
        MyContainer mc;
        mc.add(1);
        std::cout << "Rozmiar danych: " << mc.get_data().size() << std::endl;
        return 0;
    } // Destruktor MyContainer jest wywoływany, który z kolei wywołuje destruktor std::vector
    
  • Funkcje wolne i algorytmy: Rozszerzać funkcjonalność std::vector za pomocą zwykłych funkcji lub korzystając z algorytmów standardowych.

Te podejścia są bardziej elastyczne, bezpieczniejsze i zgodne z zasadami OOP oraz projektowania bibliotek w C++. Standardowe kontenery nie są przeznaczone do używania jako klasy bazowe.