Sobes.tech
Middle+

Por que o vetor funcionará mal se herdar dele?

sobes.tech IA

Resposta da IA

A herança de std::vector não é recomendada devido à ausência de destrutores virtuais e outras funções virtuais, o que viola o princípio de substituição de Liskov e leva a problemas na utilização polimórfica.

Exemplos de problemas:

  1. Problema de slicing: Ao passar um objeto de classe derivada por valor ou referência para uma classe base std::vector, os dados e comportamentos específicos da classe derivada serão perdidos.

    #include <vector>
    #include <iostream>
    
    class MyVector : public std::vector<int> {
    public:
        int my_data = 100;
        // Sem destrutor virtual
        ~MyVector() {
            std::cout << "Destrutor de MyVector" << std::endl;
        }
    };
    
    void process_vector(std::vector<int> vec) {
        // Passagem por valor - slicing
        // O destrutor de MyVector não será chamado
        std::cout << "Processando std::vector" << std::endl;
    }
    
    int main() {
        MyVector mv;
        mv.push_back(1);
        process_vector(mv); // slicing do objeto MyVector
        return 0;
    } // Aqui apenas o destrutor de std::vector será chamado
    
  2. Falta de destrutor virtual: Se você excluir um objeto de classe derivada através de um ponteiro para a classe base std::vector, o destrutor da classe derivada não será chamado, o que pode levar a vazamentos de recursos.

    #include <vector>
    #include <iostream>
    #include <memory> // Para unique_ptr
    
    class DerivedVector : public std::vector<int> {
    public:
        int* resource;
        DerivedVector() : std::vector<int>(), resource(new int) {
            std::cout << "Construtor de DerivedVector" << std::endl;
        }
        // Sem destrutor virtual
        ~DerivedVector() {
            std::cout << "Destrutor de DerivedVector" << std::endl;
            delete resource; // Pode não ser chamado
        }
    };
    
    int main() {
        // Exclusão através de um ponteiro para a classe base
        std::vector<int>* base_ptr = new DerivedVector();
        // Ao fazer delete em base_ptr, apenas o destrutor de std::vector será chamado,
        // o destrutor de DerivedVector não será chamado, causando vazamento de resource
        delete base_ptr;
    
        // Exemplo com unique_ptr
        // std::unique_ptr<std::vector<int>> up = std::make_unique<DerivedVector>();
        // up->push_back(5);
        // Ao sair do escopo, unique_ptr chamará delete no ponteiro bruto base_ptr.
        // Isso é equivalente a delete base_ptr; e causará o mesmo problema se o vector não tiver destrutor virtual.
    
        return 0;
    } // A memória alocada para resource não será liberada
    
  3. Construtores: O comportamento dos construtores de std::vector (por exemplo, cópia, movimento) pode não corresponder às expectativas para uma classe derivada se ela adicionar seu próprio estado ou lógica.

Em vez de herdar de std::vector, as melhores abordagens são:

  • Composição: Usar std::vector como membro da classe. Isso permite controlar a interface e o comportamento da nova classe, usando std::vector internamente.
    #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 { // Fornecer acesso ao vetor se necessário
            return data;
        }
        // O destrutor de MyContainer chamará corretamente o destrutor de data
        ~MyContainer() {
             std::cout << "Destrutor de MyContainer" << std::endl;
        }
    };
    
    int main() {
        MyContainer mc;
        mc.add(1);
        std::cout << "Tamanho dos dados: " << mc.get_data().size() << std::endl;
        return 0;
    } // O destrutor de MyContainer é chamado, que por sua vez chama o destrutor de std::vector
    
  • Funções livres e algoritmos: Estender a funcionalidade de std::vector com funções comuns ou usar algoritmos padrão.

Essas abordagens são mais flexíveis, seguras e seguem os princípios de POO e de design de bibliotecas em C++. Os contêineres padrão não são destinados a serem usados como classes base.