Usando punteros inteligentes para los miembros de la clase

159

Tengo problemas para entender el uso de punteros inteligentes como miembros de clase en C ++ 11. He leído mucho sobre punteros inteligentes y creo que entiendo cómo unique_ptry shared_ptr/ o weak_ptrtrabajo en general. Lo que no entiendo es el uso real. Parece que todo el mundo recomienda usarlo unique_ptrcomo el camino a seguir casi todo el tiempo. Pero, ¿cómo implementaría algo como esto?

class Device {
};

class Settings {
    Device *device;
public:
    Settings(Device *device) {
        this->device = device;
    }

    Device *getDevice() {
        return device;
    }
};    

int main() {
    Device *device = new Device();
    Settings settings(device);
    // ...
    Device *myDevice = settings.getDevice();
    // do something with myDevice...
}

Digamos que me gustaría reemplazar los punteros con punteros inteligentes. A unique_ptrno funcionaría por getDevice(), ¿verdad? ¿Entonces es el momento en que uso shared_ptry weak_ptr? No hay forma de usar unique_ptr? Me parece que para la mayoría de los casos shared_ptrtiene más sentido a menos que esté usando un puntero en un alcance realmente pequeño.

class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(std::shared_ptr<Device> device) {
        this->device = device;
    }

    std::weak_ptr<Device> getDevice() {
        return device;
    }
};

int main() {
    std::shared_ptr<Device> device(new Device());
    Settings settings(device);
    // ...
    std::weak_ptr<Device> myDevice = settings.getDevice();
    // do something with myDevice...
}

¿Es ese el camino a seguir? ¡Muchas gracias!

michaelk
fuente
44
Ayuda a ser realmente claro en cuanto a vida útil, propiedad y posibles nulos. Por ejemplo, habiendo pasado deviceal constructor de settings, ¿desea poder seguir refiriéndose a él en el ámbito de la llamada, o solo a través de settings? Si este último, unique_ptres útil. Además, ¿tiene un escenario donde el valor de retorno de getDevice()es null. Si no, solo devuelva una referencia.
Keith
2
Sí, a shared_ptres correcto en 8/10 casos. Los otros 2/10 se dividen entre unique_ptry weak_ptr. Además, weak_ptrgeneralmente se usa para romper referencias circulares; No estoy seguro de que su uso se considere correcto.
Collin Dauphinee
2
En primer lugar, ¿qué propiedad desea para el devicemiembro de datos? Primero tienes que decidir eso.
juanchopanza
1
Ok, entiendo que, como persona que llama, podría usar un unique_ptrlugar y ceder la propiedad al llamar al constructor, si sé que ya no lo necesitaré por ahora. Pero como diseñador de la Settingsclase, no sé si la persona que llama también quiere mantener una referencia. Tal vez el dispositivo se utilizará en muchos lugares. Ok, tal vez ese es exactamente tu punto. En ese caso, no sería el único propietario y es cuando usaría shared_ptr, supongo. Y: entonces los puntos inteligentes reemplazan los punteros, pero no las referencias, ¿verdad?
michaelk
esto-> dispositivo = dispositivo; También use listas de inicialización.
Nils

Respuestas:

202

A unique_ptrno funcionaría por getDevice(), ¿verdad?

No, no necesariamente Lo importante aquí es determinar la política de propiedad adecuada para su Deviceobjeto, es decir, quién será el propietario del objeto señalado por su puntero (inteligente).

¿Será la instancia del Settingsobjeto solo ? ¿El Deviceobjeto tendrá que ser destruido automáticamente cuando el Settingsobjeto sea destruido, o debería sobrevivir a ese objeto?

En el primer caso, std::unique_ptres lo que necesita, ya que es Settingsel único propietario (único) del objeto puntiagudo y el único objeto responsable de su destrucción.

Bajo este supuesto, getDevice()debería devolver un puntero de observación simple (los punteros de observación son punteros que no mantienen vivo el objeto puntiagudo). El tipo más simple de puntero de observación es un puntero sin formato:

#include <memory>

class Device {
};

class Settings {
    std::unique_ptr<Device> device;
public:
    Settings(std::unique_ptr<Device> d) {
        device = std::move(d);
    }

    Device* getDevice() {
        return device.get();
    }
};

int main() {
    std::unique_ptr<Device> device(new Device());
    Settings settings(std::move(device));
    // ...
    Device *myDevice = settings.getDevice();
    // do something with myDevice...
}

[ NOTA 1: Tal vez se pregunte por qué estoy usando punteros sin procesar aquí, cuando todos siguen diciendo que los punteros sin procesar son malos, inseguros y peligrosos. En realidad, es una advertencia preciosa, pero es importante ponerla en el contexto correcto: los punteros sin formato son malos cuando se usan para realizar la gestión manual de la memoria , es decir, asignar y desasignar objetos a través de newy delete. Cuando se usa puramente como un medio para lograr una semántica de referencia y pasar punteros de observación no propietarios, no hay nada intrínsecamente peligroso en punteros sin formato, excepto tal vez por el hecho de que uno debe tener cuidado de no desreferenciar un puntero colgante. - NOTA FINAL 1 ]

[ NOTA 2: Como surgió en los comentarios, en este caso particular donde la propiedad es única y siempre se garantiza que el objeto poseído esté presente (es decir, el miembro de datos interno devicenunca estará nullptr), la función getDevice()podría (y tal vez debería) devuelve una referencia en lugar de un puntero. Si bien esto es cierto, decidí devolver un puntero sin procesar aquí porque quise decir que se trata de una respuesta corta que se podría generalizar al caso en el que devicepodría estar nullptr, y para mostrar que los punteros sin procesar están bien siempre que no se usen para Gestión manual de la memoria. - NOTA FINAL 2 ]


La situación es radicalmente diferente, por supuesto, si su Settingsobjeto no debe tener la propiedad exclusiva del dispositivo. Este podría ser el caso, por ejemplo, si la destrucción del Settingsobjeto no implica también la destrucción del Deviceobjeto puntiagudo .

Esto es algo que solo usted como diseñador de su programa puede decir; Según el ejemplo que proporcione, es difícil para mí saber si este es el caso o no.

Para ayudarlo a resolverlo, puede preguntarse si hay otros objetos además de los Settingsque tienen derecho a mantener Devicevivo el objeto siempre que lo apunten, en lugar de ser solo observadores pasivos. Si ese es el caso, entonces necesita una política de propiedad compartida , que es lo que std::shared_ptrofrece:

#include <memory>

class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(std::shared_ptr<Device> const& d) {
        device = d;
    }

    std::shared_ptr<Device> getDevice() {
        return device;
    }
};

int main() {
    std::shared_ptr<Device> device = std::make_shared<Device>();
    Settings settings(device);
    // ...
    std::shared_ptr<Device> myDevice = settings.getDevice();
    // do something with myDevice...
}

Tenga en cuenta que weak_ptres un puntero de observación , no un puntero propietario; en otras palabras, no mantiene vivo el objeto puntiagudo si todos los demás punteros propietarios del objeto puntiagudo quedan fuera de alcance.

La ventaja de weak_ptrun puntero sin formato regular es que puede saber con seguridad si weak_ptrestá colgando o no (es decir, si está apuntando a un objeto válido o si el objeto al que apunta originalmente se ha destruido). Esto se puede hacer llamando a la expired()función miembro en el weak_ptrobjeto.

Andy Prowl
fuente
44
@LKK: Sí, correcto. A weak_ptres siempre una alternativa a los punteros de observación sin procesar. Es más seguro en cierto sentido, porque podría verificar si está colgando antes de desreferenciarlo, pero también viene con algo de sobrecarga. Si puede garantizar fácilmente que no va a desreferenciar un puntero colgante, entonces debería estar bien observando punteros en bruto
Andy Prowl
66
En el primer caso, probablemente sería mejor dejar que getDevice()devuelva una referencia, ¿no? Entonces la persona que llama no tendría que verificar nullptr.
vobject
55
@chico: No estoy seguro de lo que quieres decir. auto myDevice = settings.getDevice()creará una nueva instancia de tipo Devicellamada myDevicey la copiará-construirá a partir de la referenciada por la referencia que getDevice()devuelve. Si quieres myDeviceser una referencia, debes hacerlo auto& myDevice = settings.getDevice(). Entonces, a menos que me falte algo, estamos de vuelta en la misma situación que tuvimos sin usar auto.
Andy Prowl
2
@Purrformance: Debido a que no desea ceder la propiedad del objeto, la entrega de un objeto modificable unique_ptra un cliente abre la posibilidad de que el cliente se mude de él, adquiriendo la propiedad y dejándolo con un puntero nulo (único).
Andy Prowl
77
@Purrformance: Si bien eso evitaría que un cliente se mueva (a menos que el cliente sea un científico loco interesado const_cast), personalmente no lo haría. Expone un detalle de implementación, es decir, el hecho de que la propiedad es única y se realiza a través de a unique_ptr. Veo las cosas de esta manera: si desea / necesita pasar / devolver la propiedad, pase / devuelva un puntero inteligente ( unique_ptro shared_ptr, dependiendo del tipo de propiedad). Si no desea / necesita pasar / devolver la propiedad, use un constpuntero o referencia (debidamente calificado), principalmente dependiendo de si el argumento puede ser nulo o no.
Andy Prowl
0
class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(const std::shared_ptr<Device>& device) : device(device) {

    }

    const std::shared_ptr<Device>& getDevice() {
        return device;
    }
};

int main()
{
    std::shared_ptr<Device> device(new Device());
    Settings settings(device);
    // ...
    std::shared_ptr<Device> myDevice(settings.getDevice());
    // do something with myDevice...
    return 0;
}

week_ptrse usa solo para bucles de referencia. El gráfico de dependencia debe ser un gráfico dirigido acíclico. En los punteros compartidos hay 2 recuentos de referencia: 1 para shared_ptrsy 1 para todos los punteros ( shared_ptry weak_ptr). Cuando shared_ptrse eliminan todos los s, se elimina el puntero. Cuando se necesita un puntero weak_ptr, lockdebe usarse para obtener el puntero, si existe.

Naszta
fuente
Entonces, si entiendo su respuesta correctamente, ¿los punteros inteligentes reemplazan los punteros en bruto, pero no necesariamente las referencias?
michaelk
¿Hay en realidad dos contadores de referencia en una shared_ptr? ¿Puedes explicar por qué? Por lo que entiendo, weak_ptrno tiene que contarse porque simplemente crea uno nuevo shared_ptrcuando se opera en el objeto (si el objeto subyacente todavía existe).
Björn Pollex
@ BjörnPollex: Creé un breve ejemplo para ti: enlace . No he implementado todo, solo los constructores de copia y lock. La versión de refuerzo también es segura para subprocesos en el recuento de referencias ( deletese llama solo una vez).
Naszta
@Naszta: Su ejemplo muestra que es posible implementar esto usando dos recuentos de referencia, pero su respuesta sugiere que esto es obligatorio , lo cual no creo que sea. ¿Podría aclarar esto en su respuesta?
Björn Pollex
1
@ BjörnPollex, para weak_ptr::lock()saber si el objeto ha expirado, debe inspeccionar el "bloque de control" que contiene el primer recuento de referencias y el puntero al objeto, por lo que el bloque de control no debe destruirse mientras todavía haya weak_ptrobjetos en uso, entonces el número de weak_ptrobjetos debe ser rastreado, que es lo que hace el segundo recuento de referencia. El objeto se destruye cuando la primera cuenta de referencia cae a cero, el bloque de control se destruye cuando la segunda cuenta de referencia cae a cero.
Jonathan Wakely