Los punteros inteligentes son una funcionalidad moderna de C++ diseñada para proporcionar una gestión automática de la memoria, lo que ayuda a prevenir las fugas de memoria y los punteros colgantes que suelen asociarse a los punteros sin procesar. Actúan como envoltorios de los punteros sin procesar y añaden funcionalidad adicional, como la liberación automática de memoria cuando el puntero ya no se necesita.
Por lo general, los punteros inteligentes se implementan como plantillas de clase en la biblioteca estándar de C++.
Los dos punteros inteligentes que más se usan son std::unique_ptr y std::shared_ptr.
std::unique_ptr es un puntero inteligente que posee el objeto de forma exclusiva.
Garantiza que, en cualquier momento, solo un objeto std::unique_ptr sea dueño del recurso.
Cuando el std::unique_ptr propietario se destruye o se restablece, destruye automáticamente los objetos y libera su memoria.
#include <memory>
// Declaring and defining a unique pointer
auto rightful_king_of_england = std::make_unique<std::string>("Excalibur");
// Unique pointers cannot be copied or assigned
auto mordred = rightful_king_of_england; // Error: Cannot copy a unique_ptr
std::make_unique()
Al crear un std::unique_ptr, es preferible usar std::make_unique() en lugar de usar new directamente para asignar memoria.
std::make_unique() ofrece varias ventajas:
std::make_unique() garantiza la seguridad ante excepciones.
Si se lanza una excepción durante la construcción del objeto, la memoria se liberará automáticamente, lo que evita fugas de memoria.std::make_unique() hace que el código sea más claro y conciso.
Elimina la necesidad de especificar explícitamente el tipo que se asigna, ya que los argumentos de plantilla se deducen automáticamente.std::make_unique() de forma más eficaz que si se asigna memoria manualmente con new, lo que puede mejorar el rendimiento.std::unique_ptr se construye manualmente.
Eso daría lugar a un comportamiento indefinido cuando el std::unique_ptr intente eliminarlo al final de su ámbito.std::shared_ptr es un puntero inteligente que permite que varios objetos std::shared_ptr compartan la propiedad del mismo recurso.
Lleva la cuenta de cuántos punteros compartidos hacen referencia al recurso y libera la memoria solo cuando el último puntero compartido que posee el recurso sale de su ámbito o se restablece.
// Declaring and defining a shared pointer to a dynamically allocated string
auto martian_congressional_republic = std::make_shared<std::string>("protomolecule");
// Creating more shared pointer that shares ownership
auto outer_planets_alliance = martian_congressional_republic;
auto united_nations = martian_congressional_republic;
En C++17 y versiones anteriores, no se admite directamente el uso de std::shared_ptr con arrays mediante std::make_shared<T[]>.
Aunque es posible asignar arrays con std::make_shared<T[]>, crear punteros compartidos directamente a partir de ellos puede provocar un comportamiento indefinido debido a las diferencias en la gestión de memoria entre objetos individuales y arrays.
En su lugar, considera usar std::vector o funciones de eliminación personalizadas para gestionar arrays con punteros compartidos de forma eficaz.
Asegúrate siempre de la compatibilidad con tu compilador y con la implementación de la biblioteca estándar cuando trabajes con asignaciones de arrays y punteros compartidos en C++17.
std::make_shared()
Al igual que std::make_unique(), std::make_shared() ofrece beneficios como una mayor eficiencia de memoria, seguridad ante excepciones y legibilidad.
Combina la asignación de memoria del bloque de control y del objeto gestionado en una sola operación, lo que mejora la eficiencia y reduce el riesgo de fugas de memoria.
Además, la deducción automática de los argumentos de plantilla simplifica el código y mejora la legibilidad.
Usar std::make_shared() favorece un código más limpio, seguro y eficiente cuando trabajas con objetos std::shared_ptr en C++.
std::weak_ptr es una clase complementaria de std::shared_ptr que proporciona una referencia «débil» sin propiedad a un objeto gestionado por un puntero compartido.
Permite acceder al recurso sin afectar su tiempo de vida.
Los punteros débiles son útiles en escenarios en los que hay que romper referencias cíclicas para evitar fugas de memoria.
// Creating a shared pointer
auto your_account = std::make_shared<std::string>("secret_subscription_password");
// Creating a shared pointer that shares ownership
auto your_flatmates_account = your_account;
// Creating a weak pointer from the shared pointer
auto your_flatmates_boyfriends_account = your_flatmates_account;
// if your_account and your_flatmates_account are deleted, there is no more reference to the shared pointer.
// your_flatmates_boyfriends_account will be a null pointer and cannot use the associated object any longer.
std::weak_ptr y la propiedad cíclicastd::weak_ptr se diseñó para resolver el problema de la propiedad cíclica, también conocida como referencias circulares, que puede darse al usar std::shared_ptr.
En un escenario de propiedad cíclica, dos o más objetos std::shared_ptr se referencian entre sí y crean un ciclo en el que ninguno de los objetos se puede eliminar, porque tienen referencias fuertes entre ellos, lo que provoca fugas de memoria.
std::weak_ptr ofrece una solución a este problema, ya que permite referencias débiles a objetos compartidos sin aumentar su recuento de referencias.
Esto significa que puede observar y acceder al objeto compartido, pero no impide que se elimine.
Si se liberan todas las referencias fuertes al objeto compartido, el objeto se destruye y los punteros débiles que lo observan se restablecen automáticamente a nullptr.
Aquí tienes un ejemplo breve que muestra cómo usar std::weak_ptr para romper una propiedad cíclica:
struct Node {
Node() { std::cout << "Node constructed\n"; }
~Node() { std::cout << "Node destructed\n"; }
std::weak_ptr<Node> next;
};
// Creating two nodes
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
// Creating a cycle by making each node point to the other
node1->next = node2;
node2->next = node1;
// Release strong references to nodes
node1.reset();
node2.reset();
// Nodes are automatically destructed due to weak references
«Node constructed» se imprime dos veces, seguido de dos líneas de «Node destructed».
Cuando next no es un puntero débil sino un std::shared_ptr, se crea una situación de propiedad cíclica que provoca fugas de memoria, porque ambos nodos mantendrán referencias fuertes entre sí e impedirán su destrucción.
struct Node {
Node() { std::cout << "Node constructed\n"; }
~Node() { std::cout << "Node destructed\n"; }
std::shared_ptr<Node> next; // Change to shared_ptr
};
En este escenario, tanto node1 como node2 mantienen referencias fuertes entre sí a través de std::shared_ptr, formando un ciclo.
Cuando el programa termina e intenta liberar las referencias fuertes con reset(), los recuentos de referencias de ambos nodos siguen siendo distintos de cero, porque cada nodo mantiene una referencia fuerte al otro.
Por lo tanto, los nodos no se destruyen, lo que provoca fugas de memoria.
«Node constructed» se imprime dos veces, pero no va seguido de ninguna línea de «Node destructed».
std::weak_ptr
Los punteros colgantes se producen cuando un puntero hace referencia a un objeto que ya se eliminó, lo que provoca un comportamiento indefinido al desreferenciar el puntero.
std::weak_ptr ayuda a evitar los punteros colgantes al proporcionar una referencia débil, sin propiedad, a un objeto gestionado por std::shared_ptr.
Así puedes evitar los punteros colgantes con std::weak_ptr:
Crea un std::weak_ptr: en lugar de mantener directamente un std::shared_ptr, crea un std::weak_ptr al objeto compartido.
Comprueba la validez: antes de usar el std::weak_ptr, comprueba su validez con la función expired().
Esta función devuelve true si el objeto compartido asociado se ha eliminado.
Bloquea el std::weak_ptr: para acceder al objeto compartido de forma segura, usa la función lock(), que devuelve un std::shared_ptr que apunta al mismo objeto si este sigue siendo válido.
Si el objeto se ha eliminado, lock() devuelve un std::shared_ptr vacío.
Aquí tienes un ejemplo de código que muestra cómo usar std::weak_ptr para evitar punteros colgantes:
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
// Create weak pointers
node1->next = node2;
node2->next = node1;
// Check validity and lock
if (auto lockedNode2 = node1->next.lock()) {
// Use lockedNode2 safely
} else {
// Handle case where node2 has been deleted
}
En este ejemplo, node1->next y node2->next son objetos std::weak_ptr.
Antes de acceder al objeto compartido, se comprueba su validez con la función expired().
Luego se usa lock() para acceder al objeto compartido de forma segura.
Este enfoque garantiza que se eviten los punteros colgantes cuando trabajas con std::weak_ptr.
Usa punteros inteligentes de forma predeterminada: std::unique_ptr para la propiedad exclusiva y std::shared_ptr para la propiedad compartida.
Reserva los punteros sin procesar para referencias sin propiedad o cuando interactúes con código heredado.
En la mayoría de los casos, std::unique_ptr basta para la propiedad exclusiva, ya que ofrece una gestión de memoria ligera sin la sobrecarga del recuento de referencias.
Conviene usar std::shared_ptr con moderación, porque introduce sobrecarga y complejidad a menos que realmente se necesite una propiedad compartida.
std::weak_ptr está pensado específicamente para romper dependencias cíclicas u observar objetos compartidos, pero no se usa con frecuencia.