Sobes.tech
Middle

Como pode causar uma fuga de memória ao usar unique_ptr?

sobes.tech IA

Resposta da IA

É difícil, mas possível em certos cenários:

  1. Dependências cíclicas (propriedade compartilhada em vez de propriedade única): Se dois unique_ptr possuem objetos que se referem mutuamente, e nenhum deles é destruído primeiro, a memória não será liberada. Isso viola a ideologia do unique_ptr como proprietário exclusivo. Para esses casos, é melhor usar shared_ptr com weak_ptr.

    #include <memory>
    
    struct A;
    struct B;
    
    struct A {
        std::unique_ptr<B> ptr_b;
        A() {}
        ~A() { /* destruidor de A */ }
    };
    
    struct B {
        std::unique_ptr<A> ptr_a; // Dependência cíclica de A
        B() {}
        ~B() { /* destruidor de B */ }
    };
    
    int main() {
        auto a = std::make_unique<A>();
        auto b = std::make_unique<B>();
        // Estabelecer referências mútuas
        // Isso causará vazamento se o proprietário a ou b não destruir seu unique_ptr primeiro,
        // o que não é possível neste caso devido à propriedade mútua
        a->ptr_b = std::move(b);
        // Ao sair de main, o unique_ptr<A> a será destruído, mas seu destruidor A
        // tentará destruir ptr_b. No entanto, se ptr_b possuir um objeto B,
        // e o objeto B contiver um unique_ptr<A> que possuía o objeto A original,
        // uma dependência cíclica é criada, levando a um vazamento se não for gerenciada cuidadosamente.
        // Neste exemplo simplificado com make_unique, apenas a permanecerá no escopo
        // e seu destruidor será chamado. O vazamento ocorrerá se B possuir A, e A possuir B.
        // Aqui, b foi movido para a->ptr_b. O unique_ptr<B> b em main está agora vazio.
        // Ao sair de main, a será destruído. O destruidor de A será chamado.
        // O destruidor de unique_ptr<B> ptr_b será chamado.
        // O destruidor de B será chamado.
        // O destruidor de unique_ptr<A> ptr_a em B será chamado.
        // O problema de dependência cíclica ocorre quando os objetos não podem se destruir mutuamente
        // porque cada um possui o outro.
        // Exemplo correto de dependência cíclica:
        // struct A { std::unique_ptr<B> b; };
        // struct B { A* a_raw_ptr; }; // B conhece A, mas não possui
    
        // No caso do unique_ptr, a propriedade cíclica é extremamente incomum e difícil de criar diretamente
        // sem erros de design explícitos. Uma ligação cíclica normal não leva a um vazamento por si só
        // com unique_ptr, se não houver propriedade cíclica.
    
        // **Cenário real de vazamento com unique_ptr em contexto de ciclos:**
        // Acontece não por causa do unique_ptr em si, mas pela lógica de propriedade.
        // Por exemplo, se você tiver uma estrutura que *armazena* um unique_ptr para outra estrutura,
        // que por sua vez *armazena* um unique_ptr para a primeira, e você cria tais objetos
        // com new e os atribui a unique_ptr:
        // auto obj1 = std::make_unique<A>();
        // auto obj2 = std::make_unique<B>();
        // obj1->ptr_b = std::move(obj2); // obj1 agora possui obj2
        // obj1->ptr_b->ptr_a = std::move(obj1); // <- PROBLEMA: obj2 tenta possuir obj1,
        // que já possui obj2.
        // Isso causará um erro em tempo de execução ou um unique_ptr não inicializado,
        // não uma fuga cíclica como com shared_ptr.
    
        // **Forma mais realista de vazamento com unique_ptr em dependências:**
        // Se você usar um ponteiro bruto dentro de um objeto referenciado por um unique_ptr,
        // e esse ponteiro bruto aponta para um objeto possuído por outro unique_ptr,
        // e você esquecer de liberar o ponteiro bruto quando o segundo unique_ptr for destruído antes.
        // Mas isso não é uma fuga de unique_ptr, é uma fuga do ponteiro bruto ou uso incorreto de memória.
    
        // Vamos focar em como o próprio unique_ptr pode contribuir para uma fuga,
        // e não em erros de design fora dele.
        // A forma mais direta é por exceções.
    }
    
  2. Exceções durante a criação do objeto: Se ao criar um objeto que unique_ptr vai possuir, ou durante o construtor desse objeto, ocorrer uma exceção após a alocação de memória (new T()), mas antes de atribuir essa memória ao unique_ptr.

    #include <memory>
    
    class Resource {
    public:
        Resource() {
            // Se aqui lançar uma exceção...
            throw std::runtime_error("Erro no construtor de Resource");
        }
        ~Resource() {
            // este destruidor não será chamado se a exceção ocorrer no construtor
        }
    };
    
    int main() {
        Resource* res = nullptr;
        try {
            res = new Resource(); // memória alocada
            // ... mas uma exceção ocorre antes de o unique_ptr assumir a posse
            // std::unique_ptr<Resource> unique_resource(res); // <- até aqui o código não chegará
            std::cout << "Esta linha não será executada" << std::endl;
        } catch (const std::exception& e) {
            std::cerr << "Exceção: " << e.what() << std::endl;
            // `res` aponta para memória alocada, mas nunca foi atribuída ao
            // `unique_ptr`, nem foi liberada. Vazamento.
            // O correto seria: delete res; // no bloco catch
        }
        // Vazamento de memória se delete res; não for chamado no catch
    
        // **Como evitar vazamento neste caso:**
        // Use std::make_unique
        try {
            std::unique_ptr<Resource> safe_resource = std::make_unique<Resource>(); // RAII
            // Se ocorrer exceção no construtor de Resource, make_unique
            // gerenciará corretamente a memória alocada.
        } catch (const std::exception& e) {
            std::cerr << "Exceção gerenciada com make_unique: " << e.what() << std::endl;
            // Sem vazamento
        }
        return 0;
    }
    
  3. Deleter incorreto: Se unique_ptr estiver configurado com um deleter personalizado que não realiza sua função.

    #include <memory>
    #include <iostream>
    
    struct MyData {
        int value;
        MyData(int v) : value(v) { std::cout << "MyData(" << value << ") criado" << std::endl; }
        ~MyData() { std::cout << "MyData(" << value << ") destruído" << std::endl; }
    };
    
    // Deleter personalizado que *não* remove a memória
    struct NoOpDeleter {
        void operator()(MyData* ptr) const {
            std::cout << "NoOpDeleter chamado para " << ptr->value << ", mas sem deletar!" << std::endl;
            // delete ptr; // <- Esquecer de descomentar ou deletar intencionalmente
        }
    };
    
    int main() {
        // Criar unique_ptr com deleter personalizado
        std::unique_ptr<MyData, NoOpDeleter> data_ptr(new MyData(10));
        // Ao sair do escopo, NoOpDeleter será chamado.
        // Como NoOpDeleter não remove, a memória alocada com new MyData(10) não será liberada. Vazamento.
        return 0;
        // ~unique_ptr() chama NoOpDeleter::operator(), mas a memória não é deletada.
    }
    
  4. Chamada a .release() sem delete posterior: O método release() transfere a propriedade do ponteiro bruto. Se esse ponteiro não for posteriormente deletado explicitamente (ou passado para outro smart pointer), ocorrerá vazamento.

    #include <memory>
    #include <iostream>
    
    struct LeakyResource {
        LeakyResource() { std::cout << "LeakyResource criado" << std::endl; }
        ~LeakyResource() { std::cout << "LeakyResource destruído" << std::endl; }
    };
    
    int main() {
        std::unique_ptr<LeakyResource> ptr = std::make_unique<LeakyResource>();
    
        LeakyResource* raw_ptr = ptr.release(); // ptr agora é nullptr
        // A propriedade foi transferida para raw_ptr.
        // A memória apontada por raw_ptr não será liberada automaticamente
        // ao sair de main.
    
        // ... trabalho com raw_ptr ...
    
        // Esquecer de deletar!
        // delete raw_ptr; // <- Vazamento se esta linha estiver ausente
    
        std::cout << "ptr é " << (ptr ? "não nulo" : "nulo") << std::endl; // Mostrará "ptr é nulo"
    
        return 0; // raw_ptr ainda aponta para memória alocada, mas ela não foi liberada
    }
    

Em todos os outros cenários padrão, unique_ptr, baseado em RAII, gerencia corretamente a memória, liberando-a na saída do escopo.