cpp-shared-ptr

Qué es y cómo usar std::shared_ptr en C++

  • 4 min

Un std::shared_ptr es un smart pointer que permite compartir la propiedad de un mismo objeto entre varias partes del programa.

Ahora vamos a profundizar en uno de los tipos más flexibles, aunque a menudo malinterpretado: el std::shared_ptr.

Mientras que std::unique_ptr posee un recurso en exclusiva, std::shared_ptr permite que varios punteros compartan su propiedad.

Su filosofía es la propiedad compartida (shared ownership). Múltiples punteros pueden apuntar al mismo objeto, y el objeto solo se destruye cuando todos los punteros han desaparecido.

Cómo funciona el recuento de referencias

std::shared_ptr emplea una técnica llamada recuento de referencias (reference counting).

Internamente, un shared_ptr no solo guarda la dirección de memoria del objeto. También mantiene un puntero a un bloque de control compartido. Este bloque contiene un contador (un número entero) que indica cuántos shared_ptr están apuntando a ese objeto ahora mismo.

La lógica es la siguiente:

Cuando creas un shared_ptr, el contador se inicia en 1.

Si copias ese shared_ptr (se lo pasas a otra función, o a otro objeto), el contador aumenta.

Cuando un shared_ptr se destruye (sale del ámbito), el contador disminuye.

Si el contador llega a 0, significa que nadie necesita el objeto. Se libera la memoria automáticamente.

Las operaciones sobre el bloque de control están sincronizadas. Distintos shared_ptr que comparten ese bloque pueden copiarse y destruirse desde varios hilos, aunque esto no hace seguro el acceso simultáneo al objeto gestionado y tiene un pequeño coste.

Creación de un shared_ptr

Para usarlo, necesitamos incluir la librería <memory>.

Aunque podemos construirlo a partir de new, la forma habitual y recomendada es usar std::make_shared.

#include <memory>
#include <iostream>

int main() {
    // FORMA CORRECTA: Usando std::make_shared
    std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
    
    // FORMA NO RECOMENDADA (pero posible)
    // std::shared_ptr<int> ptr2(new int(10));
}
Copied!

¿Por qué usar std::make_shared?

Hay dos razones técnicas importantes para preferir make_shared:

  1. Eficiencia: Si usas new, haces dos asignaciones de memoria (una para el objeto y otra para el bloque de control). make_shared las hace en un solo bloque de memoria contiguo.
  2. Seguridad: Evita fugas de memoria sutiles si ocurre una excepción justo en el momento de creación de los argumentos.

Ejemplo de uso y ciclo de vida

Vamos a ver un ejemplo práctico para visualizar cómo cambia el contador de referencias usando el método .use_count().

#include <iostream>
#include <memory>

class Mensaje {
public:
    Mensaje() { std::cout << "Mensaje creado\n"; }
    ~Mensaje() { std::cout << "Mensaje destruido\n"; }
};

int main() {
    std::cout << "Inicio del programa\n";

    // 1. Creamos el primer puntero
    std::shared_ptr<Mensaje> p1 = std::make_shared<Mensaje>();
    std::cout << "Contador p1: " << p1.use_count() << "\n"; // Salida: 1

    {
        // 2. Creamos un segundo puntero COPIANDO p1
        // Ambos apuntan a lo mismo.
        std::shared_ptr<Mensaje> p2 = p1;
        std::cout << "Contador p1: " << p1.use_count() << "\n"; // Salida: 2
        std::cout << "Contador p2: " << p2.use_count() << "\n"; // Salida: 2
    
    } // 3. Aquí p2 sale del scope y se destruye. El contador baja.

    std::cout << "Contador p1 tras cerrar bloque: " << p1.use_count() << "\n"; // Salida: 1
    
    std::cout << "Fin del programa\n";
} // 4. Aquí p1 sale del scope. Contador = 0. Se llama al destructor.
Copied!

Salida del programa:

Inicio del programa
Mensaje creado
Contador p1: 1
Contador p1: 2
Contador p2: 2
Contador p1 tras cerrar bloque: 1
Fin del programa
Mensaje destruido
Copied!

Como vemos, el objeto Mensaje sobrevive al bloque interno porque p1 todavía lo estaba “sujetando”.

¿Cuándo usar shared_ptr?

Muchos programadores novatos en C++ cometen el error de usar shared_ptr para todo porque se parece a los recolectores de basura (Garbage Collectors) de lenguajes como C# o Java. No hagas esto.

El shared_ptr es más pesado que un puntero normal: suele almacenar dos punteros y además tiene la sobrecarga de gestionar el bloque de control.

Úsalo solo cuando:

  • Realmente varios objetos necesiten mantener vivo a un tercero y no esté claro quién morirá el último.
  • Ejemplos: Estructuras de datos complejas (grafos), recursos cargados en caché (texturas en un videojuego usadas por varios modelos), o paso de datos asíncronos entre hilos.

Si puedes definir un dueño claro para el objeto, usa std::unique_ptr. Si solo necesitas acceder al objeto sin poseerlo, usa un puntero crudo (observador) o una referencia.

El peligro de las referencias circulares

El problema típico de std::shared_ptr son las referencias circulares. Si el Objeto A tiene un shared_ptr al Objeto B, y el Objeto B tiene un shared_ptr al Objeto A… nunca se destruirán.

Ambos tendrán siempre el contador en 1, esperándose mutuamente. Esto genera un Memory Leak.

Para solucionarlo, C++ ofrece un observador que no aumenta el contador de propiedad: std::weak_ptr.