¿Qué tipo de puntero uso cuando?

228

Ok, entonces la última vez que escribí C ++ para ganarme la vida, std::auto_ptrera todo lo que tenía disponible la biblioteca estándar , y boost::shared_ptrestaba de moda. Realmente nunca examiné el aumento de otros tipos de punteros inteligentes proporcionados. Entiendo que C ++ 11 ahora proporciona algunos de los tipos que se le ocurrieron, pero no todos.

Entonces, ¿alguien tiene un algoritmo simple para determinar cuándo usar qué puntero inteligente? Preferiblemente incluye consejos sobre punteros tontos (como punteros crudos T*) y el resto de los punteros inteligentes de impulso. (Algo como esto sería genial).

sbi
fuente
Vea también std :: auto_ptr to std :: unique_ptr
Martin York
1
Realmente espero que a alguien se le ocurra un buen diagrama de flujo práctico como este diagrama de flujo de selección STL .
Alok Save
1
@Als: ¡Oh, esa sí que es buena! Lo hice a preguntas frecuentes.
sbi
66
@Deduplicator Eso ni siquiera está cerca de ser un duplicado. La pregunta vinculada dice "¿Cuándo debo usar un puntero inteligente" y esta pregunta es "¿Cuándo uso estos punteros inteligentes?" es decir, esta categoriza los diferentes usos de los punteros inteligentes estándar. La pregunta vinculada no hace esto. La diferencia es aparentemente pequeña pero es grande.
Rapptz

Respuestas:

183

Propiedad compartida:
El shared_ptry weak_ptrel estándar adoptado son más o menos lo mismo que sus contrapartes Boost . Úselos cuando necesite compartir un recurso y no sepa cuál será el último en estar vivo. Se usa weak_ptrpara observar el recurso compartido sin influir en su vida útil, no para romper ciclos. Los ciclos con shared_ptrnormalmente no deberían suceder: dos recursos no pueden ser dueños el uno del otro.

Tenga en cuenta que Boost también ofrece shared_array, que podría ser una alternativa adecuada a shared_ptr<std::vector<T> const>.

A continuación, Boost ofrece intrusive_ptr, que son una solución ligera si su recurso ya ofrece una gestión contada por referencia y desea adoptarla según el principio RAII. Este no fue adoptado por la norma.

Propiedad única:
Boost también tiene un scoped_ptr, que no es copiable y para el que no puede especificar un borrador. std::unique_ptrestá boost::scoped_ptren esteroides y debería ser su opción predeterminada cuando necesita un puntero inteligente . Le permite especificar un eliminador en sus argumentos de plantilla y es móvil , a diferencia boost::scoped_ptr. También es totalmente utilizable en contenedores STL siempre que no utilice operaciones que necesiten tipos copiables (obviamente).

Tenga en cuenta nuevamente que Boost tiene una versión de matriz: scoped_arrayque el estándar se unificó al requerir std::unique_ptr<T[]>una especialización parcial que utilizará delete[]el puntero en lugar de deleteutilizarlo (con la default_deleter). std::unique_ptr<T[]>también ofrece en operator[]lugar de operator*y operator->.

Tenga en cuenta que std::auto_ptrtodavía está en el estándar, pero está en desuso . §D.10 [depr.auto.ptr]

La plantilla de clase auto_ptrestá en desuso. [ Nota: La plantilla de clase unique_ptr(20.7.1) proporciona una mejor solución. —Nota final ]

Sin propiedad:
utilice punteros tontos (punteros sin procesar) o referencias para referencias que no sean propiedad de los recursos y cuando sepa que el recurso sobrevivirá al objeto / alcance de referencia. Prefiera referencias y use punteros sin procesar cuando necesite nulabilidad o reiniciabilidad.

Si desea una referencia no propietaria de un recurso, pero no sabe si el recurso sobrevivirá al objeto que hace referencia a él, empaque el recurso en ay shared_ptruse un weak_ptr- puede probar si el padre shared_ptrestá vivo lock, lo que devuelve un shared_ptrque no es nulo si el recurso todavía existe. Si desea probar si el recurso está muerto, úselo expired. Los dos pueden sonar similares, pero son muy diferentes frente a la ejecución concurrente, ya que expiredsolo garantiza su valor de retorno para esa declaración única. Una prueba aparentemente inocente como

if(!wptr.expired())
  something_assuming_the_resource_is_still_alive();

Es una posible condición de carrera.

Xeo
fuente
1
En el caso de no ser propietario, probablemente debería preferir referencias a punteros a menos que no necesite propiedad y capacidad de restablecimiento donde las referencias no lo corten, incluso entonces es posible que desee considerar reescribir el objeto original para que sea un shared_ptrpuntero no propietario. a weak_ptr...
David Rodríguez - dribeas
2
No me refería a la referencia al puntero , sino a la referencia en lugar del puntero. Si no hay propiedad, a menos que necesite reiniciabilidad (o nulabilidad, pero la nulabilidad sin poder reiniciar sería bastante limitada) puede usar una referencia simple en lugar de un puntero en primer lugar.
David Rodríguez - dribeas
1
@David: Ah, ya veo. :) Sí, las referencias no son malas para eso, personalmente también las prefiero en tales casos. Los agregaré.
Xeo
1
@Xeo: shared_array<T>es una alternativa a shared_ptr<T[]>no hacerlo shared_ptr<vector<T>>: no puede crecer.
R. Martinho Fernandes
1
@GregroyCurrie: Eso es ... ¿exactamente lo que escribí? Dije que es un ejemplo de una posible condición de carrera.
Xeo
127

Decidir qué puntero inteligente usar es una cuestión de propiedad . Cuando se trata de la gestión de recursos, el objeto A posee el objeto B si está en control de la vida útil del objeto B. Por ejemplo, las variables miembro son propiedad de sus respectivos objetos porque la vida útil de las variables miembro está vinculada a la vida útil del objeto. Elige punteros inteligentes en función de cómo se posee el objeto.

Tenga en cuenta que la propiedad en un sistema de software es independiente de la propiedad, ya que podríamos pensar fuera de él. Por ejemplo, una persona puede "ser propietaria" de su hogar, pero eso no significa necesariamente que un Personobjeto tenga control sobre la vida útil de un Houseobjeto. Combinar estos conceptos del mundo real con los conceptos de software es una forma segura de programarse en un hoyo.


Si tiene la propiedad exclusiva del objeto, úselo std::unique_ptr<T>.

Si ha compartido la propiedad del objeto ...
- Si no hay ciclos de propiedad, use std::shared_ptr<T>.
- Si hay ciclos, defina una "dirección" y úsela std::shared_ptr<T>en una dirección y std::weak_ptr<T>en la otra.

Si el objeto lo posee, pero existe la posibilidad de no tener un propietario, utilice punteros normales T*(por ejemplo, punteros principales).

Si el objeto es tuyo (o de lo contrario tiene existencia garantizada), usa referencias T&.


Advertencia: Tenga en cuenta los costos de los punteros inteligentes. En entornos con memoria o rendimiento limitado, podría ser beneficioso usar punteros normales con un esquema más manual para administrar la memoria.

Los costos:

  • Si tiene un eliminador personalizado (p. Ej., Utiliza grupos de asignación), esto generará una sobrecarga por puntero que puede evitarse fácilmente mediante la eliminación manual.
  • std::shared_ptrtiene la sobrecarga de un incremento de recuento de referencia en la copia, más una disminución en la destrucción seguida de una verificación de recuento de 0 con la eliminación del objeto retenido. Dependiendo de la implementación, esto puede inflar su código y causar problemas de rendimiento.
  • Tiempo de compilación. Al igual que con todas las plantillas, los punteros inteligentes contribuyen negativamente a los tiempos de compilación.

Ejemplos:

struct BinaryTree
{
    Tree* m_parent;
    std::unique_ptr<BinaryTree> m_children[2]; // or use std::array...
};

Un árbol binario no posee su padre, pero la existencia de un árbol implica la existencia de su padre (o nullptrde raíz), por lo que utiliza un puntero normal. Un árbol binario (con semántica de valor) tiene la propiedad exclusiva de sus hijos, por lo que son std::unique_ptr.

struct ListNode
{
    std::shared_ptr<ListNode> m_next;
    std::weak_ptr<ListNode> m_prev;
};

Aquí, el nodo de lista posee sus listas siguiente y anterior, por lo que definimos una dirección y la usamos shared_ptrpara siguiente y weak_ptranterior para romper el ciclo.

Peter Alexander
fuente
3
Para el ejemplo del árbol binario, algunas personas sugerirían usar shared_ptr<BinaryTree>para los niños y weak_ptr<BinaryTree>para la relación con los padres.
David Rodríguez - dribeas
@ DavidRodríguez-dribeas: Depende de si el Árbol tiene semántica de valor o no. Si las personas van a hacer referencia a su árbol externamente, incluso una vez que se destruye el árbol de origen, entonces sí, el combo de puntero compartido / débil sería lo mejor.
Peter Alexander
Si un objeto lo posee y se garantiza que existe, ¿por qué no una referencia?
Martin York
1
Si usa la referencia, nunca podrá cambiar el padre, lo que puede o no obstaculizar el diseño. Para equilibrar árboles, eso obstaculizaría.
Mooing Duck
3
+1 pero debe agregar una definición de "propiedad" en la primera línea. A menudo me encuentro teniendo que decir claramente que se trata de la vida y la muerte del objeto, no de la propiedad en un significado más específico del dominio.
Klaim
19

Utilizar unique_ptr<T> todo el tiempo, excepto cuando necesite un recuento de referencias, en cuyo caso use shared_ptr<T>(y en casos muy raros, weak_ptr<T>para evitar ciclos de referencia). En casi todos los casos, la propiedad única transferible está bien.

Punteros sin procesar: buenos solo si necesita retornos covariantes, puntería no propietaria que puede suceder. De lo contrario, no son terriblemente útiles.

Punteros de matriz: unique_ptrtiene una especialización para la T[]que se solicita automáticamente delete[]el resultado, por lo que puede hacerlo con seguridad, unique_ptr<int[]> p(new int[42]);por ejemplo. shared_ptraún necesitaría un eliminador personalizado, pero no necesitaría un puntero de matriz compartido o único especializado. Por supuesto, tales cosas generalmente se reemplazan mejor de std::vectortodos modos. Desafortunadamente shared_ptr, no proporciona una función de acceso a matriz, por lo que aún tendría que llamar manualmente get(), pero unique_ptr<T[]>proporciona en operator[]lugar de operator*y operator->. En cualquier caso, tienes que verificarte los límites. Esto hace que sea un shared_ptrpoco menos fácil de usar, aunque podría decirse que es una ventaja genérica y que ninguna dependencia de Boost hace unique_ptryshared_ptr los ganadores nuevamente.

Indicadores de alcance: se vuelven irrelevantes unique_ptr, al igual queauto_ptr .

Realmente no hay nada más. En C ++ 03 sin semántica de movimiento, esta situación era muy complicada, pero en C ++ 11 el consejo es muy simple.

Todavía hay usos para otros punteros inteligentes, como intrusive_ptro interprocess_ptr. Sin embargo, son muy específicos y completamente innecesarios en el caso general.

Perrito
fuente
Además, punteros en bruto para la iteración. Y para los búferes de parámetros de salida, donde el llamador es propiedad del llamador.
Ben Voigt
Hmm, la forma en que lo leí, son situaciones que son tanto de retorno covariante como de no posesión. Una reescritura podría ser buena si quisieras decir la unión en lugar de la intersección. También diría que vale la pena mencionar también la iteración.
Ben Voigt
2
std::unique_ptr<T[]>proporciona en operator[]lugar de operator*y operator->. Sin embargo, es cierto que aún debes hacer un control de ti mismo.
Xeo
8

Casos de cuándo usar unique_ptr:

  • Métodos de fábrica
  • Miembros que son punteros (incluido pimpl)
  • Almacenamiento de punteros en contenedores stl (para evitar movimientos)
  • Uso de grandes objetos dinámicos locales.

Casos de cuándo usar shared_ptr:

  • Compartir objetos a través de hilos
  • Compartir objetos en general

Casos de cuándo usar weak_ptr:

  • Mapa grande que actúa como referencia general (por ejemplo, un mapa de todos los sockets abiertos)

Siéntase libre de editar y agregar más

Lalaland
fuente
De hecho, me gusta más tu respuesta a medida que das escenarios.
Nicholas Humphrey