Introducción

Uno de los pilares fundamentales de C++ es el concepto de RAII (Resource Adquisition Is Initialization) que traducido al castellano:

La adquisición de recursos es su inicialización

Esto quiere decir que lo recursos adquiridos durante la construcción del objeto deben ser liberados cuando se destruye el objeto. Esto sumado a que los destructores de los objetos son llamados de forma automática cuando el objeto sale del ámbito de declaración, tenemos la gestión automática de recursos.

Creación de objetos con new

Como sabemos, cada vez que se crea un objeto con new se construye el objeto en memoria dinámica y debe destruirse manualmente el objeto con delete. Sino se libera un recurso se produce una fuga de memoria.

La tarea de liberar los recursos corresponde al programador y suele ser una fuente de errores constante.

¿Cómo podemos aplicar la gestión de recursos automática con objetos creados de forma dinámica?

La respuesta es smart pointers.

Smart Pointers

Los smart pointers (punteros inteligentes) son objetos cuya misión es asegurarse de que el objeto se destruye cuando no se utiliza.

¿Cómo lo hace?

Un smart pointer es un objeto que tiene la propiedad de otro a través de un puntero. Cuando el smart pointer se destruye, se asegura de llamar al destructor del objeto que posee.

std::unique_ptr<int> uniqueptr = std::unique_ptr(new int[1024]);
//obtener el puntero
int *numList = un.get();
...

Y ya está. La variable unique_ptr se encargará de liberar el array de 1024 enteros cuando se salga del ámbito.

Tenemos los punteros únicos(std::unique_ptr) y compartidos (std::shared_ptr).

unique_ptr

Los punteros únicos son de uso único al objeto al que apuntan. Cuando ya no se utilice se destruye.

shared_ptr

Los punteros compartidos se utiliza para compartir el objeto apuntado en diferentes lugares. Cuando se deje utilizar en el último lugar que obtuvo el puntero, se elimina definitivamente.

Internamente utiliza un contador de referencias que tiene en cuenta el número de veces que se solicita el puntero. Cuando ese contador llegue a cero, significa que ya nadie está usando el objeto y procede a eliminarlo de memoria.

Propiedad, punteros y referencias

Caso A (unique_ptr)

Si utilizamos un smart pointer

std::unique_ptr<A> B::getA();

¿De quién es la responsabilidad de liberar A?

Claramente nuestra. Al devolver un unique_ptr sabemos que el puntero es nuestro y es único. Nosotros somos los responsables de liberarlo. Al usar un smart pointer la liberación se hace de forma automática.

Caso B (referencia)

A& B::getA();

¿De quién es la responsabilidad de liberar A?

Al ser una referencia esta pregunta carece de sentido. Desde el punto de vista del cliente solo importa que podemos usar A, quién lo libere, o si lo libera alguna vez no importa.

Caso C (puntero crudo)

Si en algún lugar del código nos encontramos con esto:

A* B::getA();

¿De quién es la responsabilidad de liberar A?

¿Es responsabilidad de B? ¿Es propiedad del código cliente que lo obtiene?

A priori, no sabríamos responder a la pregunta. Necesitamos leer la documentación para saber como proceder.

Propiedad

En todos los casos, podemos transformar le pregunta de responsabilidad por propiedad. El propietario del objeto es el responsable de liberar sus recursos.

Ejemplos

Veamos unos ejemplos sencillos

shared pointer

#include <iostream>
#include <memory>
#include <string>

class Person
{
    public:
        Person(std::string name): name{name} { 
            std::cout << "ctor::" << name << std::endl;
         };
        ~Person() { 
            std::cout << "dtor::" << name << std::endl;
        };
    private:
        std::string name;
};

int main()
{
    auto p1 = std::make_shared<Person>("p1");
    auto p2 = std::make_shared<Person>("p2");

    std::cout << "before -> p1 = p2" << std::endl;
    p1 = p2; //III
    std::cout << "after -> p1 = p2" << std::endl;
    return 0;
}

Que resulta en:

ctor::p1 ctor::p2 before -> p1 = p2 dtor::p1 after -> p1 = p2 dtor::p2

Como se puede observar, lo interesante ocurre cuando se hace la asignación: p1=p2. Lo que ocurre es que el constructor de copy assignment del smart pointer ejecuta el destructor de la Persona de nombre “p1” para copiar Persona “p2”.

Esto es genial porque la memoria se gestiona de forma “automatica”. El desarrollador no tiene que preocuparse de borrar la memoria de forma manual.

unique pointer

#include <iostream>
#include <memory>
#include <string>

class Person
{
    public:
        Person(std::string name): name{name} { 
            std::cout << "ctor::" << name << std::endl;
         };
        ~Person() { 
            std::cout << "dtor::" << name << std::endl;
        };
    private:
        std::string name;
};

int main()
{
    auto p1 = std::make_unique<Person>("p1");
    auto p2 = std::make_unique<Person>("p2");

    std::cout << "before -> p1 = p2" << std::endl;
    p1 = p2;
    std::cout << "after -> p1 = p2" << std::endl;
    return 0;
}

El compilador nos lanza un error al hacer p1=p2. ¿Por qué? Porque los unique_ptr por definición no permiten las copias (porque son punteros únicos). Si quitamos la linea conflictiva, tanto p1 como p2 se eliminarán cuando salgan del scope.

Conclusión

Las referencias y los smart pointers revelan mejor la intencionalidad y la propiedad sin lugar a dudas.

Sin embargo un puntero crudo deja margen a dudas.