¿Cómo pasar std :: unique_ptr?

83

Estoy teniendo mi primer intento de usar C ++ 11 unique_ptr; Estoy reemplazando un puntero crudo polimórfico dentro de un proyecto mío, que es propiedad de 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);

Porque la persona que llama tendría que manejar la propiedad del puntero a la función, lo que no quiero. Entonces, ¿cuál es la mejor solución a 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 no parece instintivo pasar algo ya escrito _ptrcomo referencia, lo que sería una referencia de una 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ían necesarias dos indirecciones de puntero consecutivas para llegar a mi variable.

lvella
fuente

Respuestas:

97

Si desea que la función use el puntero, pase una referencia a él. No hay 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 uso del sitio de la llamada operator*:

func(*some_unique_ptr, 42);

Alternativamente, si basese permite que el argumento sea nulo, mantenga la firma como está y use la get()función miembro:

bool func(BaseClass* base, int other_arg);
func(some_unique_ptr.get(), 42);
R. Martinho Fernandes
fuente
4
Esto es bueno, pero ¿puedes deshacerte del std::unique_ptrpara std::vector<std::unique_ptr>discutir?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
31

La ventaja de usar std::unique_ptr<T>(además de no tener que recordar llamar deleteo delete[]explícitamente) es que garantiza que un puntero es nullptro apunta a una instancia válida del objeto (base). Volveré a esto después de responder a su pregunta, pero el primer mensaje es DEBE usar punteros inteligentes para administrar la vida útil de los objetos asignados dinámicamente.

Ahora, su problema es en realidad 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 constcalificadores, según sea necesario):

bool func(BaseClass& ref, int other_arg) { ... }

Luego, la persona que llama, que tiene un std::shared_ptr<BaseClass> ptr, manejará el nullptrcaso o le pedirá 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 llamador debe prometer que la referencia es válida y que seguirá siendo válida durante la ejecución del cuerpo de la función.


Aquí está la razón por la que creo firmemente que debe no pasar punteros primas o referencias a punteros inteligentes.

Un puntero sin formato es solo una dirección de memoria. Puede tener uno de (al menos) 4 significados:

  1. La dirección de un bloque de memoria donde se encuentra el objeto deseado. ( el bueno )
  2. 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 )
  3. La dirección de un bloque de memoria que está fuera del espacio direccionable de su proceso (desreferenciarlo con suerte hará que su programa se bloquee). ( el feo )
  4. La dirección de un bloque de memoria que se puede desreferenciar pero que no contiene lo que esperabas. Tal vez el puntero se modificó accidentalmente y ahora apunta a otra dirección de escritura (de una variable completamente diferente dentro de su proceso). Escribir en esta ubicación de memoria hará que suceda mucha diversión, a veces, durante la ejecución, porque el sistema operativo no se quejará siempre que se le permita escribir allí. ( ¡Zoinks! )

El uso correcto de punteros inteligentes alivia los casos bastante aterradores 3 y 4, que generalmente no son detectables en tiempo de compilación y que generalmente solo experimenta en tiempo de ejecución cuando su programa falla o hace cosas inesperadas.

Pasar punteros inteligentes como argumentos tiene dos desventajas: no se puede cambiar el carácter constdel objeto señalado sin hacer una copia (lo que agrega sobrecarga shared_ptry no es posible unique_ptr), y aún 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.

Imagínese lo que significa cuando una función recibe a nullptrcomo parámetro. Primero tiene que decidir qué hacer con él: ¿usar un valor "mágico" en lugar del objeto perdido? cambiar el comportamiento por completo y calcular otra cosa (que no requiere el objeto)? entrar en pánico y lanzar una excepción? Además, ¿qué sucede cuando la función toma 2, 3 o incluso más argumentos por puntero sin formato? Tiene que comprobar cada uno de ellos y adaptar su comportamiento en consecuencia. Esto agrega un nivel completamente nuevo a la validación de entrada sin una 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, solo debe aceptar la promesa de la persona que llama de que la memoria a la que se apunta es segura para trabajar según lo previsto. (Las referencias siguen siendo direcciones de memoria, pero representan conceptualmente una promesa de validez).

Victor Savu
fuente
25

Estoy de acuerdo con Martinho, pero creo que es importante señalar la semántica de propiedad de un pase por referencia. Creo que la solución correcta es usar una simple pasada por referencia aquí:

bool func(BaseClass& base, int other_arg);

El significado comúnmente aceptado de un paso por referencia en C ++ es como si el llamador de la función le dijera a la función "aquí, puede tomar prestado este objeto, usarlo y modificarlo (si no es constante), pero solo para el duración de la función del cuerpo ". Esto, de ninguna manera, está en conflicto con las reglas de propiedad del unique_ptrporque el objeto simplemente está siendo prestado por un corto período de tiempo, no se está produciendo una transferencia de propiedad real (si presta su automóvil a alguien, ¿firma el título a él?).

Entonces, aunque pueda parecer malo (en cuanto al diseño, prácticas de codificación, etc.) sacar la referencia (o incluso el puntero en bruto) fuera de la unique_ptr, en realidad no lo es porque está perfectamente de acuerdo con las reglas de propiedad establecidas por el unique_ptr. Y luego, por supuesto, hay otras ventajas interesantes, como una sintaxis limpia, sin restricciones a solo objetos propiedad de a unique_ptr, y así.

Mikael Persson
fuente
0

Personalmente, evito sacar una referencia de un puntero / puntero inteligente. Porque, ¿qué pasa si el puntero es nullptr? Si cambia la firma a esta:

bool func(BaseClass& base, int other_arg);

Es posible que deba proteger su código de las desreferencias de puntero nulo:

if (the_unique_ptr)
  func(*the_unique_ptr, 10);

Si la clase es la única dueña del puntero, la segunda alternativa de Martinho parece más razonable:

func(the_unique_ptr.get(), 10);

Alternativamente, puede usar std::shared_ptr. Sin embargo, si hay una sola entidad responsable delete, los std::shared_ptrgastos generales no se amortizan.

Guilherme Ferreira
fuente
¿Sabes que, en la mayoría de ocasiones, std::unique_ptrtiene cero gastos generales, verdad?
lvella
1
Sí, lo sé. Por eso le hablé de los std::shared_ptrgastos generales.
Guilherme Ferreira