conceptC++ Moderno~5 min de lecturaActualizado 2026-07-02#cpp#smart-pointers#unique-ptr#shared-ptr#ownership

Smart pointers: unique_ptr, shared_ptr, ownership

Un smart pointer no es "un puntero que no puede estar mal". Es un objeto que guarda un puntero y ejecuta una política de ownership en su destructor. std::unique_ptr<T> significa que exactamente un owner va a borrar el objeto. std::shared_ptr<T> significa que un control block cuenta owners compartidos y borra el objeto cuando se va el último. std::weak_ptr<T> observa un objeto administrado por shared_ptr sin extender su lifetime. El punto no es reemplazar todos los punteros; es hacer visibles los punteros owning.

El reset: los punteros crudos y las referencias están bien para borrow. Los smart pointers son para ownership.

Cómo funciona de verdad

std::unique_ptr<T> es el puntero owning por defecto. Es move-only, así que pasarlo por valor significa transferencia. Destruirlo llama a su deleter sobre el puntero guardado. En código normal, eso significa delete p; con arrays significa delete[]; con un deleter custom puede significar fclose, close, munmap o una función destroy de una biblioteca C. Un unique pointer suele tener el mismo tamaño que un puntero crudo cuando el deleter no tiene estado.

std::shared_ptr<T> es otra herramienta. Guarda o apunta a un control block que contiene el contador de owners fuertes, el contador de observadores weak, el deleter y a menudo estado del allocator. Copiar un shared_ptr incrementa el contador fuerte. Destruir uno lo decrementa. Cuando el contador fuerte llega a cero, el objeto administrado se destruye. Cuando tanto strong como weak counts desaparecen, el control block puede desalocarse. Esas actualizaciones de contadores tienen overhead y son thread-safe respecto de objetos shared_ptr distintos que comparten el mismo control block.

std::weak_ptr<T> existe porque el shared ownership puede formar ciclos. Si A owns B con un shared_ptr y B owns A con otro shared_ptr, ambos contadores pueden quedar distintos de cero después de que el mundo exterior suelta el grafo. Un weak pointer rompe ese borde de ownership. Llamás lock() para intentar ownership temporario; devuelve un shared_ptr vacío si el objeto ya no existe.

Situación Tipo a usar Por qué
Una función borrowea un objeto requerido T& o const T& no transfiere ownership, no puede ser null
Una función borrowea un objeto opcional T* borrow nullable, no implica deletion
Una función crea y devuelve ownership std::unique_ptr<T> o un valor la transferencia es explícita
Un objeto tiene un owner pero debe moverse std::unique_ptr<T> copy está deshabilitado, move transfiere ownership
El lifetime es genuinamente compartido std::shared_ptr<T> el último owner destruye
Back-pointer u observación de cache std::weak_ptr<T> observa sin mantener vivo

Artefacto ejecutable: transferencia explícita de ownership

El demo ejecutable vive en examples/modern-cpp/smart-pointers-unique-ptr-shared-ptr-ownership/. Compara un protocolo C de transferencia de owner con Packet ** contra la versión C++ con std::unique_ptr, std::shared_ptr y std::weak_ptr.

cd examples/modern-cpp/smart-pointers-unique-ptr-shared-ptr-ownership
./run.sh

El lado C tiene que documentar la transferencia por convención:

static Packet *packet_take(Packet **slot) {
    Packet *packet = *slot;
    *slot = NULL;
    return packet;
}

El lado C++ convierte la transferencia en un hecho de compile time:

auto owner = make_packet("unique");
auto next_owner = std::move(owner);

std::cout << (owner ? "not null" : "null") << "\n";
std::cout << next_owner->payload << "\n";

std::move no mueve por sí mismo; castea owner a un xvalue para que se seleccione el move constructor o move assignment. Para std::unique_ptr, ese move transfiere el puntero guardado y deja vacío el origen. El programa C puede olvidarse de nullear el owner viejo; el tipo C++ lo hace como parte de su operación de move.

La parte compartida del demo usa un observador std::weak_ptr:

std::weak_ptr<CacheEntry> observer;
{
    auto shared = std::make_shared<CacheEntry>("cache-line");
    observer = shared;
    auto second_handle = shared;
}

std::cout << observer.expired() << "\n";

Después de que ambos owners fuertes salen del scope, CacheEntry se destruye y el observador weak reporta expiración en vez de quedar colgando.

Fallas típicas y trade-offs

  • No uses shared_ptr como default. Hace el ownership menos local, agrega tráfico de reference count y puede esconder incertidumbre arquitectónica.
  • Los ciclos hacen leak. Dos objetos que se own entre sí mediante shared_ptr pueden mantenerse vivos para siempre. Usá weak_ptr para parent links, observers, caches y back edges.
  • shared_ptr<T>(raw) dos veces es un bug de double-delete. Dos control blocks independientes creen que own el mismo puntero crudo. Usá make_shared, make_unique o pasá smart pointers existentes.
  • get() es un borrow, no una transferencia. Pasar ptr.get() a código que guarda o borra el puntero rompe el modelo de ownership.
  • Los deleters custom son parte del tipo para unique_ptr. Eso puede afectar APIs y tamaño de objetos. Vale la pena cuando envolvés handles C.
  • Los arrays necesitan el owner correcto. Usá std::vector<T> para la mayoría de arrays dinámicos. Si realmente necesitás un smart pointer a array, usá std::unique_ptr<T[]>, no std::unique_ptr<T>.

En la práctica

  • Valores de retorno antes que ownership en heap. Si un tipo es barato o movable, devolvelo por valor y dejá trabajar a move/copy elision.
  • Usá make_unique y make_shared. Evitan new desnudo, hacen inmediato el ownership y reducen agujeros de exception safety.
  • Pasá unique_ptr por valor solo para transferir. Para borrow, pasá T&, const T& o T*. Para reseatear un owner, pasá std::unique_ptr<T>&.
  • Pasá shared_ptr por valor solo para compartir lifetime. Si la función solo necesita acceso, pasá una referencia a T en vez de incrementar un reference count.
  • Modelá ownership en los fields. Un data member puntero crudo debería hacer que la review pregunte: "¿esto es borrow, borrow opcional, índice a otro owner o un bug?"

Conecta con: RAII: la idea que cambia todo · Move semantics y value categories · Qué es realmente un puntero · Punteros a punteros · Memory leaks y disciplina de ownership · Use-after-free y double-free · Arrays y array-to-pointer decay

Fuentes

  • cppreference - std::unique_ptr - ownership exclusivo move-only, deleters, especialización de arrays y operaciones miembro. https://en.cppreference.com/w/cpp/memory/unique_ptr
  • cppreference - std::shared_ptr - shared ownership, control blocks, reference counts y make_shared. https://en.cppreference.com/w/cpp/memory/shared_ptr
  • cppreference - std::weak_ptr - observación no-owning de objetos shared_ptr y ruptura de ciclos. https://en.cppreference.com/w/cpp/memory/weak_ptr
  • C++ working draft - [unique.ptr] - wording de biblioteca estándar para unique_ptr. https://eel.is/c++draft/unique.ptr
  • C++ working draft - [util.smartptr.shared] - wording de biblioteca estándar para shared_ptr. https://eel.is/c++draft/util.smartptr.shared
  • C++ working draft - [util.smartptr.weak] - wording de biblioteca estándar para weak_ptr. https://eel.is/c++draft/util.smartptr.weak
  • C++ Core Guidelines - reglas de ownership como preferir unique_ptr y representar ownership explícitamente. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines