Smart Pointers para C++ – Weak Pointers

por Fabio A. Mazzarino

Smart Pointers foi um conceito adicionado no C++11 para reduzir os riscos inerentes ao desenvolvimento utilizando ponteiros, como por exemplo referência cíclica entre atributos. No post de hoje vamos aprender como os Weak Pointers podem resolver esse problema quando utilizando Shared Pointers.

Para entender melhor o problema de referência cíclica vamos fazer um exemplo aonde o shared_ptr funciona perfeitamente, uma lista ligada:

Lista Ligada Simples

#include <iostream>
#include <memory>
class SLinked;
class Node {
public:
    std::shared_ptr<Node> back;
    unsigned data;

    Node(std::shared_ptr<Node> b, unsigned d) : back(b), data(d) {
        std::cout << "constructor data: " << data << std::endl;
    }
    ~Node() {
        std::cout << "destructor data: " << data << std::endl;
    }
    friend class SLinked;
};
class SLinked {
public:
    std::shared_ptr<Node> end;
    SLinked() : end(nullptr) { }
    void append(unsigned data) {
        if (!end)
            end = std::make_shared<Node>(nullptr, data);
        else
            end = std::make_shared<Node>(end, data);
    }
};
int main() {
    SLinked linked;
    linked.append(5);
    linked.append(10);
    linked.append(15);
    linked.append(20);
}

Utilizando shared_ptr evitamos o risco de não fazer a deleção dos ponteiros. Assim, sem nenhuma ação de liberação de memória nos destrutores vamos ter a seguinte saída:

constructor data: 5
constructor data: 10
constructor data: 15
constructor data: 20
destructor data: 20
destructor data: 15
destructor data: 10
destructor data: 5

Lista Ligada Dupla

No segundo exemplo vamos implementar uma lista ligada dupla, continuando utilizando shared_ptr, o que vai gerar uma referência cíclica entre os nós. Vamos ao código problemático:

#include <iostream>
#include <memory>
class DLinked;
class Node {
public:
    std::shared_ptr<Node> back;
    std::shared_ptr<Node> forward;
    unsigned data;
    Node(std::shared_ptr<Node> b, std::shared_ptr<Node> f, unsigned d)
    : back(b), forward(f), data(d) {
        std::cout << "constructor data: " << data << std::endl;
    }
    ~Node() {
        std::cout << "destructor data: " << data << std::endl;
    }

};
class DLinked {
public:
    std::shared_ptr<Node> begin;
    std::shared_ptr<Node> end;
    DLinked() : begin(nullptr), end(nullptr) { }
    void push_back(unsigned data) {
        if (!begin || !end) {
            begin = end = std::make_shared<Node>(nullptr, nullptr, data);
        } else {
            end = end->forward = std::make_shared<Node>(end, nullptr, data);

        }
    }
    void push_front(unsigned data) {
        if (!begin || !end) {
            begin = end = std::make_shared<Node>(nullptr, nullptr, data);
        } else {
            begin = begin->back = std::make_shared<Node>(nullptr, begin, data);
        }
    }
};
int main() {
    DLinked linked;
    linked.push_back(5);
    linked.push_back(10);
    linked.push_back(15);
    linked.push_back(20);
}

Apesar da lista ligada simples ter dado tudo certo, a saída da lista ligada dupla a saída será a seguinte:

constructor data: 5
constructor data: 10
constructor data: 15
constructor data: 20

Note que nenhum destrutor foi executado, portanto houve vazamento de memória. Isso ocorre por conta da referência cíclica pois os objetosl nodes apontam-se uns aos outros. Para resolver é necessário utilizar o weak_ptr.

Corrigindo a Lista Ligada Dupla

Para resolver o problema da referência cíclica é necessário alterar o smart_ptr que faz a referência cíclica por um weak_ptr. Em resumo, o weak_ptr se vale de um shared_ptr porém sem incrementar o contador de referências, evitando o vazamento de memória.

#include <iostream>
#include <memory>
class DLinked;
class Node {
public:
    std::weak_ptr<Node> back;
    std::shared_ptr<Node> forward;
    unsigned data;

    Node(std::weak_ptr<Node> b, std::shared_ptr<Node> f, unsigned d)
    : back(b), forward(f), data(d) {
        std::cout << "constructor data: " << data << std::endl;
    }
    ~Node() {
        std::cout << "destructor data: " << data << std::endl;
    }

};
class DLinked {
public:
    std::shared_ptr<Node> begin;
    std::shared_ptr<Node> end;
    DLinked()  { begin.reset(); end.reset(); }
    void push_back(unsigned data) {
        if (!begin || !end) {
            begin = end = std::make_shared<Node>(std::weak_ptr<Node>(), nullptr, data);
        } else {
            end = end->forward = std::make_shared<Node>(end, nullptr, data);
        }
    }
    void push_front(unsigned data) {
        if (!begin || !end) {
            begin = end = std::make_shared<Node>(std::weak_ptr<Node>(), nullptr, data);
        } else {
            begin->back = std::make_shared<Node>(std::weak_ptr<Node>(), begin, data);
            begin = begin->back.lock();
        }
    }
};

int main() {
    DLinked linked;
    linked.push_back(5);
    linked.push_back(10);
    linked.push_back(15);
    linked.push_back(20);
}

Que irá gerar a saída:

constructor data: 5
constructor data: 10
constructor data: 15
constructor data: 20
destructor data: 5
destructor data: 10
destructor data: 15
destructor data: 20

E agora é possível verificar que os destrutores são devidamente executados, indicando que não houve vazamento de memória.