crudo, débil_ptr, único_ptr, compartido_ptr etc. ¿Cómo elegirlos sabiamente?

33

Hay muchos punteros en C ++, pero para ser honesto en 5 años más o menos en la programación de C ++ (específicamente con Qt Framework), solo uso el viejo puntero sin formato:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

Sé que hay muchos otros punteros "inteligentes":

// shared pointer:
shared_ptr<SomeKindofObject> Object;

// unique pointer:
unique_ptr<SomeKindofObject> Object;

// weak pointer:
weak_ptr<SomeKindofObject> Object;

Pero no tengo la menor idea de qué hacer con ellos y qué pueden ofrecerme en comparación con los punteros en bruto.

Por ejemplo, tengo este encabezado de clase:

#ifndef LIBRARY
#define LIBRARY

class LIBRARY
{
public:
    // Permanent list that will be updated from time to time where
    // each items can be modified everywhere in the code:
    QList<ItemThatWillBeUsedEveryWhere*> listOfUselessThings; 
private:
    // Temporary reader that will read something to put in the list
    // and be quickly deleted:
    QSettings *_reader;
    // A dialog that will show something (just for the sake of example):
    QDialog *_dialog;
};

#endif 

Esto claramente no es exhaustivo, pero para cada uno de estos 3 punteros, ¿está bien dejarlos "en bruto" o debería usar algo más apropiado?

Y en la segunda vez, si un empleador leerá el código, ¿será estricto con el tipo de punteros que uso o no?

CheshireChild
fuente
Este tema parece muy apropiado para SO. Esto fue en 2008 . Y aquí está ¿Qué tipo de puntero uso cuando? . Estoy seguro de que puedes encontrar mejores coincidencias. Estos fueron solo los primeros que vi
sehe
imo este límite ya que se trata tanto del significado conceptual / intención de estas clases como de los detalles técnicos de su comportamiento e implementaciones. Como la respuesta aceptada se inclina hacia la primera, estoy contento de que esta sea la "versión PSE" de esa pregunta SO.
Ixrec

Respuestas:

70

Un puntero "en bruto" no está administrado. Es decir, la siguiente línea:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

... perderá memoria si deleteno se ejecuta un acompañamiento en el momento adecuado.

auto_ptr

Con el fin de minimizar estos casos, std::auto_ptr<>se introdujo. Sin embargo, debido a las limitaciones de C ++ anteriores al estándar de 2011, aún es muy fácil auto_ptrperder memoria. Sin embargo, es suficiente para casos limitados como este:

void func() {
    std::auto_ptr<SomeKindOfObject> sKOO_ptr(new SomeKindOfObject());
    // do some work
    // will not leak if you do not copy sKOO_ptr.
}

Uno de sus casos de uso más débiles es en contenedores. Esto se debe a que si se realiza una copia de un auto_ptr<>y la copia anterior no se restablece cuidadosamente, el contenedor puede eliminar el puntero y perder datos.

unique_ptr

Como reemplazo, C ++ 11 introdujo std::unique_ptr<>:

void func2() {
    std::unique_ptr<SomeKindofObject> sKOO_unique(new SomeKindOfObject());

    func3(sKOO_unique); // now func3() owns the pointer and sKOO_unique is no longer valid
}

Tal unique_ptr<>se limpiará correctamente, incluso cuando se pasa entre funciones. Lo hace representando semánticamente la "propiedad" del puntero: el "propietario" lo limpia. Esto lo hace ideal para usar en contenedores:

std::vector<std::unique_ptr<SomeKindofObject>> sKOO_vector();

A diferencia auto_ptr<>, unique_ptr<>se comporta bien aquí, y cuando se vectorcambia el tamaño, ninguno de los objetos se eliminará accidentalmente mientras las vectorcopias se almacenan.

shared_ptr y weak_ptr

unique_ptr<>es útil, sin duda, pero hay casos en los que desea que dos partes de su base de código puedan hacer referencia al mismo objeto y copiar el puntero, sin dejar de garantizar la limpieza adecuada. Por ejemplo, un árbol podría verse así, cuando se usa std::shared_ptr<>:

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

En este caso, incluso podemos conservar múltiples copias de un nodo raíz, y el árbol se limpiará correctamente cuando se destruyan todas las copias del nodo raíz.

Esto funciona porque cada uno shared_ptr<>mantiene no solo el puntero al objeto, sino también un recuento de referencia de todos los shared_ptr<>objetos que hacen referencia al mismo puntero. Cuando se crea uno nuevo, el recuento aumenta. Cuando uno es destruido, la cuenta baja. Cuando el recuento llega a cero, el puntero es deleted.

Esto presenta un problema: las estructuras de doble enlace terminan con referencias circulares. Digamos que queremos agregar un parentpuntero a nuestro árbol Node:

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Ahora, si eliminamos a Node, hay una referencia cíclica a él. Nunca será deleted porque su recuento de referencia nunca será cero.

Para resolver este problema, utiliza un std::weak_ptr<>:

template<class T>
struct Node {
    T value;
    std::weak_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Ahora, las cosas funcionarán correctamente y eliminar un nodo no dejará referencias atascadas en el nodo principal. Sin embargo, hacer que caminar por el árbol sea un poco más complicado:

std::shared_ptr<Node<T>> parent_of_this = node->parent.lock();

De esta manera, puede bloquear una referencia al nodo, y tiene una garantía razonable de que no desaparecerá mientras está trabajando en él, ya que está aferrándose a uno shared_ptr<>de ellos.

make_shared y make_unique

Ahora, hay algunos problemas menores con shared_ptr<>y unique_ptr<>que deben abordarse. Las siguientes dos líneas tienen un problema:

foo_unique(std::unique_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());
foo_shared(std::shared_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());

Si thrower()arroja una excepción, ambas líneas perderán memoria. Y más que eso, shared_ptr<>mantiene el recuento de referencia lejos del objeto al que apunta y esto puede significar una segunda asignación). Eso no suele ser deseable.

C ++ 11 proporciona std::make_shared<>()y C ++ 14 proporciona std::make_unique<>()para resolver este problema:

foo_unique(std::make_unique<SomeKindofObject>(), thrower());
foo_shared(std::make_shared<SomeKindofObject>(), thrower());

Ahora, en ambos casos, incluso si thrower()arroja una excepción, no habrá una pérdida de memoria. Como beneficio adicional, make_shared<>()tiene la oportunidad de crear su recuento de referencia en el mismo espacio de memoria que su objeto administrado, lo que puede ser más rápido y ahorrar algunos bytes de memoria, ¡al tiempo que le ofrece una garantía de seguridad excepcional!

Notas sobre Qt

Sin embargo, debe tenerse en cuenta que Qt, que debe ser compatible con compiladores anteriores a C ++ 11, tiene su propio modelo de recolección de basura: muchos QObjecttienen un mecanismo en el que serán destruidos adecuadamente sin la necesidad del usuario delete.

No sé cómo QObjectse comportarán los mensajes de correo electrónico administrados por C ++ 11, por lo que no puedo decir que shared_ptr<QDialog>sea ​​una buena idea. No tengo suficiente experiencia con Qt para decirlo con certeza, pero creo que Qt5 se ha ajustado para este caso de uso.

Greyfade
fuente
1
@Zilators: Tenga en cuenta mi comentario agregado sobre Qt. La respuesta a su pregunta sobre si los tres punteros se deben administrar depende de si los objetos Qt se comportarán bien.
greyfade
2
"ambos hacen una asignación por separado para mantener el puntero"? No, unique_ptr nunca asigna nada extra, solo shared_ptr debe asignar un recuento de referencia + objeto-asignador. ¿"ambas líneas perderán memoria"? no, solo podría, ni siquiera una garantía de mal comportamiento.
Deduplicador
1
@Deduplicator: Mi redacción debe haber sido poco clara: shared_ptres un objeto separado, una asignación separada, del newobjeto ed. Existen en diferentes lugares. make_sharedtiene la capacidad de juntarlos en la misma ubicación, lo que mejora la localidad de caché, entre otras cosas.
greyfade
2
@greyfade: Nononono. shared_ptrEs un objeto. Y para administrar un objeto, debe asignar un objeto (recuentos de referencia (débil + fuerte) + destructor). make_sharedpermite asignar eso y el objeto administrado como una sola pieza. unique_ptrno los usa, por lo que no hay una ventaja correspondiente, aparte de asegurarse de que el objeto siempre sea propiedad del puntero inteligente. Por otro lado, uno puede tener un shared_ptrque posee un objeto subyacente y representa un nullptr, o que no posee y representa un puntero no nulo.
Deduplicador
1
Lo miré y parece haber una confusión general sobre lo que shared_ptrhace: 1. Comparte la propiedad de algún objeto (representado por un objeto interno asignado dinámicamente que tiene un recuento de referencias débil y fuerte, así como un eliminador) . 2. Contiene un puntero. Esas dos partes son independientes. make_uniquey make_sharedambos se aseguran de que el objeto asignado se coloque de forma segura en un puntero inteligente. Además, make_sharedpermite asignar el objeto de propiedad y el puntero administrado juntos.
Deduplicador