¿Cómo pasar std::unique_ptr?

7 minutos de lectura

avatar de usuario
lvella

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 como _ptr como referencia, lo que sería una referencia de una referencia. En segundo lugar, porque la firma de la función se vuelve aún más grande. En tercer lugar, porque en el código generado, serían necesarias dos direcciones indirectas de puntero consecutivas para llegar a mi variable.

Si desea que la función use la punta, pásele una referencia. 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 llamada operator*:

func(*some_unique_ptr, 42);

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

bool func(BaseClass* base, int other_arg);
func(some_unique_ptr.get(), 42);

  • Esto es bueno, pero ahora te deshaces del std::unique_ptr para std::vector<std::unique_ptr> ¿argumento?

    – Ciro Santilli Путлер Капут 六四事

    10 de diciembre de 2016 a las 17:15

La ventaja de usar std::unique_ptr<T> (aparte de no tener que recordar llamar delete o delete[] explícitamente) es que garantiza que un puntero es 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 use punteros inteligentes para administrar la vida útil de los objetos asignados dinámicamente.

Ahora tu problema es en realidad cómo usar esto con tu código anterior.

Mi sugerencia es que si no desea transferir o compartir la propiedad, debe siempre pasar referencias al objeto. Declare su función así (con o sin const calificadores, según sea necesario):

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

Entonces la persona que llama, que tiene un std::shared_ptr<BaseClass> ptr manejará el nullptr caso o preguntará bool func(...) para calcular 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 tiene que promesa que la referencia es válida y que seguirá siendo válida durante la ejecución del cuerpo de la función.


Esta es la razón por la que creo firmemente que deberías no pasar punteros sin procesar o referencias a punteros inteligentes.

Un puntero sin procesar 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 podría 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 (si se elimina la referencia, es de esperar que el programa se bloquee). (el feo)
  4. La dirección de un bloque de memoria que se puede desreferenciar pero que no contiene lo que espera. 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 los 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 puede cambiar el const-ness de la puntiagudo objeto sin hacer una copia (lo que agrega gastos generales para shared_ptr y no es posible para unique_ptr), y todavía te queda el segundo (nullptr) sentido.

Marqué el segundo caso como (el malo) desde una perspectiva de diseño. Este es un argumento más sutil sobre la responsabilidad.

Imagina 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 que falta? cambiar el comportamiento por completo y calcular algo más (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 procesar? Tiene que comprobar 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 ningún motivo real.

La persona que llama debe ser la que tenga suficiente información contextual para tomar estas decisiones o, en otras palabras, el malo es menos aterrador cuanto más sabes. La función, por otro lado, solo debe tomar la promesa de la persona que llama de que la memoria a la que apunta es segura para trabajar según lo previsto. (Las referencias siguen siendo direcciones de memoria, pero conceptualmente representan una promesa de validez).

Estoy de acuerdo con Martinho, pero creo que es importante señalar la semántica de propiedad de una referencia de paso. Creo que la solución correcta es usar un paso por referencia simple 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 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 corporal”. Esto no está, de ninguna manera, en conflicto con las reglas de propiedad de la unique_ptr debido a que el objeto simplemente se toma prestado por un corto período de tiempo, no se produce una transferencia de propiedad real (si le presta su automóvil a alguien, ¿le cede el título?).

Por lo tanto, aunque parezca malo (en términos de diseño, prácticas de codificación, etc.) sacar la referencia (o incluso el puntero sin procesar) del unique_ptren 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 agradables, como una sintaxis limpia, sin restricciones a solo los objetos que pertenecen a un unique_ptry entonces.

Personalmente, evito sacar una referencia de un puntero/puntero inteligente. Porque, ¿qué sucede si el puntero está nullptr? Si cambia la firma a esto:

bool func(BaseClass& base, int other_arg);

Es posible que deba proteger su código de desreferencias de punteros nulos:

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

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

func(the_unique_ptr.get(), 10);

Como alternativa, puede utilizar std::shared_ptr. Sin embargo, si hay una sola entidad responsable de deletela std::shared_ptr los gastos generales no compensan.

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad