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_ptrcomo 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_ptrpueden mantenerse vivos para siempre. Usáweak_ptrpara 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_uniqueo pasá smart pointers existentes.get()es un borrow, no una transferencia. Pasarptr.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[]>, nostd::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_uniqueymake_shared. Evitannewdesnudo, hacen inmediato el ownership y reducen agujeros de exception safety. - Pasá
unique_ptrpor valor solo para transferir. Para borrow, pasáT&,const T&oT*. Para reseatear un owner, pasástd::unique_ptr<T>&. - Pasá
shared_ptrpor valor solo para compartir lifetime. Si la función solo necesita acceso, pasá una referencia aTen 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 ymake_shared. https://en.cppreference.com/w/cpp/memory/shared_ptr - cppreference -
std::weak_ptr- observación no-owning de objetosshared_ptry ruptura de ciclos. https://en.cppreference.com/w/cpp/memory/weak_ptr - C++ working draft -
[unique.ptr]- wording de biblioteca estándar paraunique_ptr. https://eel.is/c++draft/unique.ptr - C++ working draft -
[util.smartptr.shared]- wording de biblioteca estándar parashared_ptr. https://eel.is/c++draft/util.smartptr.shared - C++ working draft -
[util.smartptr.weak]- wording de biblioteca estándar paraweak_ptr. https://eel.is/c++draft/util.smartptr.weak - C++ Core Guidelines - reglas de ownership como preferir
unique_ptry representar ownership explícitamente. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines