c++ - smart - ¿Cómo pasar std:: unique_ptr around?
shared_ptr (4)
Estoy de acuerdo con Martinho, pero creo que es importante señalar la semántica de propiedad de un pase de referencia. Creo que la solución correcta es usar una simple referencia de paso aquí:
bool func(BaseClass& base, int other_arg);
El significado comúnmente aceptado de una referencia de paso en C ++ es como si la persona que llama a la función le dice a la función "aquí, puede tomar prestado este objeto, usarlo y modificarlo (si no es const), pero solo para el duración del cuerpo de la función ". Esto de ninguna manera entra en conflicto con las reglas de propiedad de unique_ptr
porque el objeto simplemente se toma prestado por un corto período de tiempo, no hay transferencia de propiedad real (si le prestas el coche a alguien, ¿firmas el título a él?).
Entonces, aunque pueda parecer malo (diseño, prácticas de codificación, etc.) extraer la referencia (o incluso el puntero sin unique_ptr
) de unique_ptr
, en realidad no es porque esté perfectamente de acuerdo con las reglas de propiedad establecidas por el unique_ptr
. Y luego, por supuesto, hay otras bonitas ventajas, como la sintaxis limpia, no hay restricción para solo los objetos propiedad de un unique_ptr
, y así sucesivamente.
Estoy teniendo mi primer intento de usar C ++ 11 unique_ptr
; Estoy reemplazando un puntero polimérico en bruto dentro de un proyecto mío, que pertenece a una clase, pero que se transmite con bastante frecuencia.
Solía tener funciones como:
bool func(BaseClass* ptr, int other_arg) {
bool val;
// plain ordinary function that does something...
return val;
}
Pero pronto me di cuenta de que no podría cambiar a:
bool func(std::unique_ptr<BaseClass> ptr, int other_arg);
Debido a que la persona que llama tendría que manejar la propiedad del puntero a la función, lo que yo no quiero. Entonces, ¿cuál es la mejor solución para mi problema?
Pensé en pasar el puntero como referencia, así:
bool func(const std::unique_ptr<BaseClass>& ptr, int other_arg);
Pero me siento muy incómodo al hacerlo, en primer lugar porque parece no instintivo pasar algo que ya está escrito como referencia como referencia, lo que sería una referencia de referencia. En segundo lugar, porque la firma de la función se hace aún más grande. En tercer lugar, porque en el código generado, sería necesario dos indirectos de puntero consecutivos para llegar a mi variable.
La ventaja de utilizar std::unique_ptr<T>
(aparte de no tener que recordar llamar a delete
o delete[]
explícitamente) es que garantiza que un puntero sea nullptr
o apunta a una instancia válida del objeto (base) . Volveré a esto después de responder a su pregunta, pero el primer mensaje es HACER uso de punteros inteligentes para administrar la vida útil de los objetos dinámicamente asignados.
Ahora, su problema es cómo usar esto con su código anterior .
Mi sugerencia es que si no desea transferir o compartir la propiedad, siempre debe pasar referencias al objeto. Declare su función de esta manera (con o sin calificadores const
, según sea necesario):
bool func(BaseClass& ref, int other_arg) { ... }
Entonces la persona que llama, que tiene una std::shared_ptr<BaseClass> ptr
manejará el caso nullptr
o le pedirá a bool func(...)
que calcule el resultado:
if (ptr) {
result = func(*ptr, some_int);
} else {
/* the object was, for some reason, either not created or destroyed */
}
Esto significa que cualquier persona que llama debe prometer que la referencia es válida y que continuará siendo válida durante la ejecución del cuerpo de la función.
Esta es la razón por la que creo firmemente que no debe pasar punteros brutos ni referencias a punteros inteligentes.
Un puntero sin formato es solo una dirección de memoria. Puede tener uno de (al menos) 4 significados:
- La dirección de un bloque de memoria donde se encuentra el objeto deseado. ( lo bueno )
- La dirección 0x0 de la que puede estar seguro no es desreferenciable y puede tener la semántica de "nada" o "ningún objeto". ( el malo )
- La dirección de un bloque de memoria que está fuera del espacio direccionable de su proceso (desreferenciarlo hará que su programa falle). ( el feo )
- La dirección de un bloque de memoria que puede ser desreferenciado pero que no contiene lo que espera. Tal vez el puntero fue modificado accidentalmente y ahora apunta a otra dirección grabable (de una variable completamente diferente dentro de su proceso). Escribir en esta ubicación de memoria causará mucha diversión, a veces, durante la ejecución, ya que el sistema operativo no se quejará siempre que se le permita escribir allí. ( ¡Zoinks! )
El uso correcto de punteros inteligentes alivia los casos más bien aterradores 3 y 4, que generalmente no se detectan en el momento de la compilación y que generalmente solo se experimentan en tiempo de ejecución cuando el programa falla o hace cosas inesperadas.
Pasar punteros inteligentes como argumentos tiene dos desventajas: no se puede cambiar la const
-ness del objeto puntiagudo sin hacer una copia (lo que agrega sobrecarga para shared_ptr
y no es posible para unique_ptr
), y aún se queda con el segundo ( nullptr
) significado .
Marqué el segundo caso como ( el malo ) desde una perspectiva de diseño. Este es un argumento más sutil sobre la responsabilidad.
Imagine lo que significa cuando una función recibe un nullptr
como su parámetro. Primero tiene que decidir qué hacer con él: ¿usar un valor "mágico" en lugar del objeto perdido? cambiar completamente el comportamiento y calcular algo más (que no requiere el objeto)? pánico y lanzar una excepción? Además, ¿qué sucede cuando la función toma 2, o 3 o incluso más argumentos por puntero sin formato? Tiene que verificar cada uno de ellos y adaptar su comportamiento en consecuencia. Esto agrega un nivel completamente nuevo además de la validación de entrada sin ninguna razón real.
La persona que llama debe ser la que tenga suficiente información contextual para tomar estas decisiones, o, en otras palabras, lo malo es menos aterrador cuanto más sepa. La función, por otro lado, debería tomar la promesa del que llama de que la memoria a la que apunta es segura para que funcione de la forma prevista. (Las referencias siguen siendo direcciones de memoria, pero conceptualmente representan una promesa de validez).
Personalmente, evito hacer una referencia desde un puntero / puntero inteligente. Porque, ¿qué sucede si el puntero es nullptr
? Si cambia la firma a esto:
bool func(BaseClass& base, int other_arg);
Es posible que tengas que proteger tu código de las desreferencias de puntero nulo:
if (the_unique_ptr)
func(*the_unique_ptr, 10);
Si la clase es el único propietario del puntero, el segundo de la alternativa de Martinho parece más razonable:
func(the_unique_ptr.get(), 10);
Alternativamente, puede usar std::shared_ptr
. Sin embargo, si hay una única entidad responsable de delete
, la std::shared_ptr
no da sus frutos.
Si desea que la función use el punto, pase una referencia al mismo. No hay ninguna razón para vincular la función para que funcione solo con algún tipo de puntero inteligente:
bool func(BaseClass& base, int other_arg);
Y en el sitio de llamada use el operator*
:
func(*some_unique_ptr, 42);
Alternativamente, si se permite que el argumento base
sea nulo, conserve la firma como está, y use la función de miembro get()
:
bool func(BaseClass* base, int other_arg);
func(some_unique_ptr.get(), 42);