Sobes.tech
Middle+

Γιατί ο διανυσματικός θα λειτουργεί άσχημα αν κληρονομηθεί από αυτόν;

sobes.tech AI

Απάντηση από AI

Η κληρονομικότητα από το std::vector δεν συνιστάται λόγω της απουσίας εικονικών καταστροφέων και άλλων εικονικών συναρτήσεων, κάτι που παραβιάζει την αρχή υποκατάστασης του Λίσκοφ (LSP) και οδηγεί σε προβλήματα κατά τη χρήση πολυμορφισμού.

Παραδείγματα προβλημάτων:

  1. Πρόβλημα slicing: Όταν μεταβιβάζετε ένα αντικείμενο της παράγωγης κλάσης μέσω τιμής ή αναφοράς στην βασική κλάση std::vector, τα δεδομένα και η συμπεριφορά που είναι συγκεκριμένα για την παράγωγη κλάση χάνονται.

    #include <vector>
    #include <iostream>
    
    class MyVector : public std::vector<int> {
    public:
        int my_data = 100;
        // Χωρίς εικονικό καταστροφέα
        ~MyVector() {
            std::cout << "MyVector καταστροφέας" << std::endl;
        }
    };
    
    void process_vector(std::vector<int> vec) {
        // Λαμβάνεται με τιμή - συμβαίνει slicing
        // Ο καταστροφέας του MyVector δεν θα κληθεί
        std::cout << "Επεξεργασία std::vector" << std::endl;
    }
    
    int main() {
        MyVector mv;
        mv.push_back(1);
        process_vector(mv); // Συμβαίνει slicing του αντικειμένου MyVector
        return 0;
    } // Εδώ θα κληθεί μόνο ο καταστροφέας του std::vector
    
  2. Έλλειψη εικονικού καταστροφέα: Αν διαγράψετε ένα αντικείμενο της παράγωγης κλάσης μέσω δείκτη στην βασική κλάση std::vector, ο καταστροφέας της παράγωγης κλάσης δεν θα κληθεί, κάτι που μπορεί να οδηγήσει σε διαρροές πόρων.

    #include <vector>
    #include <iostream>
    #include <memory> // Για unique_ptr
    
    class DerivedVector : public std::vector<int> {
    public:
        int* resource;
        DerivedVector() : std::vector<int>(), resource(new int) {
            std::cout << "Constructor DerivedVector" << std::endl;
        }
        // Χωρίς εικονικό καταστροφέα
        ~DerivedVector() {
            std::cout << "Destructor DerivedVector" << std::endl;
            delete resource; // Μπορεί να μην κληθεί
        }
    };
    
    int main() {
        // Διαγραφή μέσω δείκτη στην βασική κλάση
        std::vector<int>* base_ptr = new DerivedVector();
        // Με delete base_ptr καλείται μόνο ο καταστροφέας του std::vector,
        // ο καταστροφέας του DerivedVector δεν καλείται, υπάρχει διαρροή πόρων
        delete base_ptr;
    
        // Παράδειγμα με unique_ptr
        // std::unique_ptr<std::vector<int>> up = std::make_unique<DerivedVector>();
        // Όταν βγει από το scope, το unique_ptr θα καλέσει delete στον raw δείκτη base_ptr.
        // Αυτό είναι ισοδύναμο με delete base_ptr; και θα έχει το ίδιο πρόβλημα αν ο vector δεν έχει εικονικό καταστροφέα.
    
        return 0;
    } // Η μνήμη που έχει δεσμευτεί για το resource δεν θα ελευθερωθεί
    
  3. Κατασκευαστές: Η συμπεριφορά των κατασκευαστών του std::vector (π.χ. κατασκευαστής αντιγραφής, μεταφοράς) μπορεί να μην ανταποκρίνεται στις προσδοκίες για μια παράγωγη κλάση αν αυτή προσθέτει δική της κατάσταση ή λογική.

Αντί να κληρονομήσετε από το std::vector, οι καλύτερες προσεγγίσεις είναι:

  • Σύνθεση: Χρησιμοποιήστε το std::vector ως μέλος της κλάσης. Αυτό σας επιτρέπει να ελέγχετε το interface και τη συμπεριφορά της νέας κλάσης, χρησιμοποιώντας το std::vector εσωτερικά.
    #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 { // Παρέχει πρόσβαση στο vector αν χρειάζεται
            return data;
        }
        // Ο καταστροφέας της MyContainer θα καλέσει σωστά τον καταστροφέα του data
        ~MyContainer() {
             std::cout << "Destructor MyContainer" << std::endl;
        }
    };
    
    int main() {
        MyContainer mc;
        mc.add(1);
        std::cout << "Μέγεθος δεδομένων: " << mc.get_data().size() << std::endl;
        return 0;
    } // Ο καταστροφέας της MyContainer καλείται, που με τη σειρά του καλεί τον καταστροφέα του std::vector
    
  • Ελεύθερες συναρτήσεις και αλγόριθμοι: Επεκτείνετε τη λειτουργικότητα του std::vector με συνηθισμένες συναρτήσεις ή χρησιμοποιήστε τους τυπικούς αλγόριθμους.

Αυτές οι προσεγγίσεις είναι πιο ευέλικτες, ασφαλέστερες και συμμορφώνονται με τις αρχές της αντικειμενοστραφούς προγραμματισμού και του σχεδιασμού βιβλιοθηκών σε C++. Τα τυπικά containers δεν προορίζονται για χρήση ως βασικές κλάσεις.