Saltar al contenido

¿Cómo paso un argumento unique_ptr a un constructor o una función?

Nuestro equipo de especialistas pasados algunos días de trabajo y de recopilar de información, hemos dado con los datos necesarios, esperamos que resulte de gran utilidad para tu plan.

Solución:

Aquí están las posibles formas de tomar un puntero único como argumento, así como su significado asociado.

(A) Por valor

Base(std::unique_ptr n)
  : next(std::move(n)) 

Para que el usuario llame a esto, debe realizar una de las siguientes acciones:

Base newBase(std::move(nextBase));
Base fromTemp(std::unique_ptr(new Base(...));

Tomar un puntero único por valor significa que está transfiriendo propiedad del puntero a la función / objeto / etc en cuestión. Después newBase esta construido, nextBase está garantizado para ser vacío. No eres el propietario del objeto y ya ni siquiera tienes un puntero hacia él. Se fue.

Esto está asegurado porque tomamos el parámetro por valor. std::move en realidad no moverse cualquier cosa; es solo un elenco elegante. std::move(nextBase) devuelve un Base&& que es una referencia de valor r a nextBase. Eso es todo lo que hace.

Porque Base::Base(std::unique_ptr n) toma su argumento por valor en lugar de por referencia de valor r, C ++ construirá automáticamente un temporal para nosotros. Crea un std::unique_ptr desde el Base&& que le dimos la función a través de std::move(nextBase). Es la construcción de este temporal lo que en realidad se mueve el valor de nextBase en el argumento de la función n.

(B) Por referencia de valor l no constante

Base(std::unique_ptr &n)
  : next(std::move(n)) 

Esto debe invocarse en un valor l real (una variable con nombre). No se puede llamar con un temporal como este:

Base newBase(std::unique_ptr(new Base)); //Illegal in this case.

El significado de esto es el mismo que el significado de cualquier otro uso de referencias no constantes: la función puede o puede que no reclamar la propiedad del puntero. Dado este código:

Base newBase(nextBase);

No hay garantía de que nextBase esta vacio. Eso mayo estar vacío puede que no. Realmente depende de lo que Base::Base(std::unique_ptr &n) quiere hacer. Por eso, no es muy evidente solo por la firma de la función lo que va a suceder; tienes que leer la implementación (o documentación asociada).

Por eso, no sugeriría esto como una interfaz.

(C) Por referencia de valor l constante

Base(std::unique_ptr const &n);

No muestro una implementación, porque tú no poder pasar de un const&. Pasando un const&, está diciendo que la función puede acceder al Base a través del puntero, pero no puede Tienda en cualquier lugar. No puede reclamar su propiedad.

Esto puede resultar útil. No necesariamente para su caso específico, pero siempre es bueno poder pasarle un puntero a alguien y saber que no poder (sin romper las reglas de C ++, como no desechar const) reclamar la propiedad de la misma. No pueden almacenarlo. Pueden pasarlo a otros, pero esos otros tienen que seguir las mismas reglas.

(D) Por referencia de valor r

Base(std::unique_ptr &&n)
  : next(std::move(n)) 

Esto es más o menos idéntico al caso “por referencia de valor l no constante”. Las diferencias son dos cosas.

  1. usted pueden pasar un temporal:

    Base newBase(std::unique_ptr(new Base)); //legal now..
    
  2. usted debe usar std::move al pasar argumentos no temporales.

Este último es realmente el problema. Si ve esta línea:

Base newBase(std::move(nextBase));

Tiene una expectativa razonable de que, una vez completada esta línea, nextBase debe estar vacío. Debería haberse movido de. Después de todo, tienes eso std::move sentado allí, diciéndole que se ha producido un movimiento.

El problema es que no lo ha hecho. No lo es garantizado haber sido movido de. Eso mayo se han movido de, pero solo lo sabrá mirando el código fuente. No se puede saber solo por la firma de la función.

Recomendaciones

  • (A) Por valor: Si te refieres a una función para reclamar propiedad de un unique_ptr, tómelo por valor.
  • (C) Por referencia de valor l constante: Si quiere decir que una función simplemente use el unique_ptr por la duración de la ejecución de esa función, tómelo por const&. Alternativamente, pase un & o const& al tipo real al que se apunta, en lugar de utilizar un unique_ptr.
  • (D) Por referencia de valor r: Si una función puede o no reclamar la propiedad (dependiendo de las rutas de código interno), tómela por &&. Pero recomiendo encarecidamente no hacer esto siempre que sea posible.

Cómo manipular unique_ptr

No puede copiar un unique_ptr. Solo puedes moverlo. La forma correcta de hacer esto es con el std::move función de biblioteca estándar.

Si tomas un unique_ptr por valor, puede moverse libremente. Pero el movimiento en realidad no ocurre debido a std::move. Toma la siguiente declaración:

std::unique_ptr newPtr(std::move(oldPtr));

Estas son realmente dos declaraciones:

std::unique_ptr &&temporary = std::move(oldPtr);
std::unique_ptr newPtr(temporary);

(nota: El código anterior no se compila técnicamente, ya que las referencias de valor r no temporales no son en realidad valores r. Está aquí solo con fines de demostración).

los temporary es solo una referencia de valor r a oldPtr. Eso esta en el constructor de newPtr donde ocurre el movimiento. unique_ptr‘s move constructor (un constructor que toma un && a sí mismo) es lo que hace el movimiento real.

Si tienes un unique_ptr valor y desea almacenarlo en algún lugar, debe usar std::move para hacer el almacenamiento.

Permítanme intentar establecer los diferentes modos viables de pasar punteros a objetos cuya memoria es administrada por una instancia del std::unique_ptr plantilla de clase; también se aplica a los mayores std::auto_ptr plantilla de clase (que creo que permite todos los usos que hace el puntero único, pero para los cuales, además, se aceptarán valores l modificables donde se esperan valores r, sin tener que invocar std::move), y en cierta medida también a std::shared_ptr.

Como ejemplo concreto para la discusión, consideraré el siguiente tipo de lista simple

struct node;
typedef std::unique_ptr list;
struct node  int entry; list next; 

Las instancias de dicha lista (que no pueden compartir partes con otras instancias o ser circular) son propiedad exclusiva de quien tenga la inicial list puntero. Si el código de cliente sabe que la lista que almacena nunca estará vacía, también puede optar por almacenar la primera node directamente en lugar de un list. No destructor para node debe definirse: dado que los destructores de sus campos se llaman automáticamente, el destructor de puntero inteligente eliminará de forma recursiva toda la lista una vez que finalice la vida útil del puntero o nodo inicial.

Este tipo recursivo da la oportunidad de discutir algunos casos que son menos visibles en el caso de un puntero inteligente a datos sin formato. Además, las funciones mismas proporcionan ocasionalmente (de forma recursiva) un ejemplo de código de cliente. El typedef para list está, por supuesto, sesgado hacia unique_ptr, pero la definición podría cambiarse para usar auto_ptr o shared_ptr en su lugar, sin mucha necesidad de cambiar a lo que se dice a continuación (especialmente en lo que respecta a la seguridad de las excepciones que se garantiza sin la necesidad de escribir destructores).

Modos de pasar punteros inteligentes

Modo 0: pasa un puntero o argumento de referencia en lugar de un puntero inteligente

Si su función no está relacionada con la propiedad, este es el método preferido: no haga que tome un puntero inteligente en absoluto. En este caso su función no necesita preocuparse OMS posee el objeto al que se apunta, o por qué medios se administra la propiedad, por lo que pasar un puntero sin formato es perfectamente seguro y la forma más flexible, ya que, independientemente de la propiedad, un cliente siempre puede producir un puntero sin formato (ya sea llamando al get método o desde la dirección del operador &).

Por ejemplo, a la función para calcular la longitud de dicha lista, no se le debe dar un list argumento, pero un puntero sin formato:

size_t length(const node* p)
 size_t l=0; for ( ; p!=nullptr; p=p->next.get()) ++l; return l; 

Un cliente que tiene una variable list head puede llamar a esta función como length(head.get()), mientras que un cliente que ha optado por almacenar un node n que representa una lista no vacía puede llamar length(&n).

Si se garantiza que el puntero no es nulo (lo cual no es el caso aquí, ya que las listas pueden estar vacías), es posible que se prefiera pasar una referencia en lugar de un puntero. Podría ser un puntero / referencia a noconst si la función necesita actualizar el contenido de los nodos, sin agregar o eliminar ninguno de ellos (esto último implicaría la propiedad).

Un caso interesante que cae en la categoría del modo 0 es hacer una copia (profunda) de la lista; Si bien una función que hace esto debe, por supuesto, transferir la propiedad de la copia que crea, no se preocupa por la propiedad de la lista que está copiando. Entonces podría definirse de la siguiente manera:

list copy(const node* p)
 return list( p==nullptr ? nullptr : new nodep->entry,copy(p->next.get()) ); 

Este código merece una mirada de cerca, tanto por la pregunta de por qué se compila (el resultado de la llamada recursiva a copy en la lista del inicializador se une al argumento de referencia rvalue en el constructor de movimiento de unique_ptr, también conocido como list, al inicializar el next campo del generado node), y para la pregunta de por qué es seguro para excepciones (si durante el proceso de asignación recursivo se agota la memoria y alguna llamada de new lanza std::bad_alloc, entonces, en ese momento, un puntero a la lista construida parcialmente se mantiene de forma anónima en un tipo temporal list creado para la lista de inicializadores, y su destructor limpiará esa lista parcial). Por cierto, uno debe resistir la tentación de reemplazar (como hice inicialmente) el segundo nullptr por p, que después de todo se sabe que es nulo en ese punto: no se puede construir un puntero inteligente a partir de un puntero (sin formato) a constante, incluso cuando se sabe que es nulo.

Modo 1: pasar un puntero inteligente por valor

Una función que toma un valor de puntero inteligente como argumento toma posesión del objeto apuntado de inmediato: el puntero inteligente que la persona que llama tenía (ya sea en una variable nombrada o un temporal anónimo) se copia en el valor del argumento en la entrada de la función y la persona que llama el puntero se ha vuelto nulo (en el caso de un temporal, la copia podría haberse elidido, pero en cualquier caso, la persona que llama ha perdido el acceso al objeto apuntado). Me gustaría llamar a este modo llamar en efectivo: la persona que llama paga por adelantado el servicio llamado y no puede hacerse ilusiones sobre la propiedad después de la llamada. Para aclarar esto, las reglas del idioma requieren que la persona que llama envuelva el argumento en std::move si el puntero inteligente se mantiene en una variable (técnicamente, si el argumento es un valor l); en este caso (pero no para el modo 3 a continuación) esta función hace lo que sugiere su nombre, es decir, mover el valor de la variable a temporal, dejando la variable nula.

Para los casos en los que la función llamada toma posesión incondicionalmente de (roba) el objeto apuntado, este modo se usa con std::unique_ptr o std::auto_ptr es una buena forma de pasar un puntero junto con su propiedad, lo que evita cualquier riesgo de pérdida de memoria. No obstante, creo que hay muy pocas situaciones en las que el modo 3 a continuación no se prefiera (aunque sea ligeramente) sobre el modo 1. Por esta razón, no proporcionaré ejemplos de uso de este modo. (Pero mira el reversed ejemplo del modo 3 a continuación, donde se observa que el modo 1 funcionaría al menos tan bien.) Si la función toma más argumentos que solo este puntero, puede suceder que haya además una razón técnica para evitar el modo 1 (con std::unique_ptr o std::auto_ptr): dado que se lleva a cabo una operación de movimiento real al pasar una variable de puntero p por la expresión std::move(p), no se puede suponer que p tiene un valor útil al evaluar los otros argumentos (el orden de evaluación no está especificado), lo que podría conducir a errores sutiles; Por el contrario, el uso del modo 3 asegura que no se mueva de p tiene lugar antes de la llamada a la función, por lo que otros argumentos pueden acceder de forma segura a un valor a través de p.

Cuando se usa con std::shared_ptr, este modo es interesante porque con una única definición de función permite a la persona que llama escoger si mantener una copia compartida del puntero para sí mismo mientras se crea una nueva copia compartida para ser utilizada por la función (esto sucede cuando se proporciona un argumento lvalue; el constructor de copia para punteros compartidos usados ​​en la llamada aumenta el recuento de referencias), o para simplemente darle a la función una copia del puntero sin retener uno o tocar el recuento de referencia (esto sucede cuando se proporciona un argumento rvalue, posiblemente un lvalue envuelto en una llamada de std::move). Por ejemplo

void f(std::shared_ptr x) // call by shared cash
 container.insert(std::move(x));  // store shared pointer in container

void client()
 std::shared_ptr p = std::make_shared(args);
  f(p); // lvalue argument; store pointer in container but keep a copy
  f(std::make_shared(args)); // prvalue argument; fresh pointer is just stored away
  f(std::move(p)); // xvalue argument; p is transferred to container and left null

Lo mismo podría lograrse definiendo por separado void f(const std::shared_ptr& x) (para el caso de lvalue) y void f(std::shared_ptr&& x) (para el caso rvalue), con cuerpos de función que difieren solo en que la primera versión invoca semántica de copia (usando construcción / asignación de copia cuando se usa x) pero la segunda versión mueve la semántica (escribiendo std::move(x) en su lugar, como en el código de ejemplo). Entonces, para punteros compartidos, el modo 1 puede ser útil para evitar la duplicación de código.

Modo 2: pasar un puntero inteligente por referencia de valor l (modificable)

Aquí, la función solo requiere tener una referencia modificable al puntero inteligente, pero no da ninguna indicación de lo que hará con él. Me gustaría llamar a este método llamar con tarjeta: la persona que llama asegura el pago dando un número de tarjeta de crédito. La referencia pueden utilizarse para tomar posesión del objeto apuntado, pero no es necesario. Este modo requiere proporcionar un argumento lvalue modificable, correspondiente al hecho de que el efecto deseado de la función puede incluir dejar un valor útil en la variable del argumento. Un llamador con una expresión rvalue que desea pasar a dicha función se vería obligado a almacenarla en una variable con nombre para poder realizar la llamada, ya que el lenguaje solo proporciona conversión implícita a una constante Referencia de lvalue (refiriéndose a un temporal) de un rvalue. (A diferencia de la situación opuesta manejada por std::move, un elenco de Y&& para Y&, con Y el tipo de puntero inteligente, no es posible; no obstante, esta conversión podría obtenerse mediante una función de plantilla simple si realmente se desea; consulte https://stackoverflow.com/a/24868376/1436796). En el caso de que la función llamada intente apropiarse incondicionalmente del objeto, robando del argumento, la obligación de proporcionar un argumento lvalue está dando la señal incorrecta: la variable no tendrá ningún valor útil después de la llamada. Por lo tanto, el modo 3, que ofrece posibilidades idénticas dentro de nuestra función pero pide a los llamantes que proporcionen un valor r, debería ser el preferido para tal uso.

Sin embargo, existe un caso de uso válido para el modo 2, a saber, funciones que pueden modificar el puntero, o el objeto apuntado de una manera que implique la propiedad. Por ejemplo, una función que antepone un nodo a un list proporciona un ejemplo de dicho uso:

void prepend (int x, list& l)  l = list( new node x, std::move(l) ); 

Claramente, aquí no sería deseable obligar a las personas que llaman a usar std::move, ya que su puntero inteligente aún posee una lista bien definida y no vacía después de la llamada, aunque diferente a la anterior.

Nuevamente es interesante observar lo que sucede si el prepend la llamada falla por falta de memoria libre. Entonces el new la llamada arrojará std::bad_alloc; en este momento, ya que no node podría asignarse, es seguro que la referencia rvalue pasada (modo 3) de std::move(l) aún no puede haber sido robado, ya que eso se haría para construir el next campo de la node que no pudo ser asignado. Entonces, el puntero inteligente original l aún conserva la lista original cuando se lanza el error; esa lista será destruida correctamente por el destructor de puntero inteligente, o en caso de l debería sobrevivir gracias a una catch cláusula, todavía mantendrá la lista original.

Ese fue un ejemplo constructivo; con un guiño a esta pregunta, también se puede dar el ejemplo más destructivo de eliminar el primer nodo que contiene un valor dado, si lo hubiera:

void remove_first(int x, list& l)
 list* p = &l;
  while ((*p).get()!=nullptr and (*p)->entry!=x)
    p = &(*p)->next;
  if ((*p).get()!=nullptr)
    (*p).reset((*p)->next.release()); // or equivalent: *p = std::move((*p)->next); 

Una vez más, la corrección es bastante sutil aquí. En particular, en la declaración final, el puntero (*p)->next que se mantiene dentro del nodo que se va a eliminar se desvincula (por release, que devuelve el puntero pero hace que el original sea nulo) antes dereset (implícitamente) destruye ese nodo (cuando destruye el antiguo valor retenido por p), asegurándose de que uno y sólo uno nodo se destruye en ese momento. (En la forma alternativa mencionada en el comentario, este tiempo se dejaría a las partes internas de la implementación del operador de asignación de movimiento del std::unique_ptr ejemplo list; el estándar dice 20.7.1.2.3; 2 que este operador debe actuar “como si estuviera llamando reset(u.release())“, por lo que el tiempo también debería ser seguro aquí).

Tenga en cuenta que prepend y remove_first no puede ser llamado por clientes que almacenan un local node variable para una lista siempre no vacía, y con razón, ya que las implementaciones dadas no podrían funcionar para tales casos.

Modo 3: pasar un puntero inteligente por referencia rvalue (modificable)

Este es el modo preferido para usar cuando simplemente se toma posesión del puntero. Me gustaría llamar a este método llamar por cheque: la persona que llama debe aceptar renunciar a la propiedad, como si estuviera proporcionando efectivo, firmando el cheque, pero el retiro real se pospone hasta que la función llamada realmente roba el puntero (exactamente como lo haría cuando se usa el modo 2). La “firma del cheque” significa concretamente que las personas que llaman tienen que envolver un argumento en std::move (como en el modo 1) si es un valor l (si es un valor r, la parte de “ceder la propiedad” es obvia y no requiere un código separado).

Tenga en cuenta que técnicamente el modo 3 se comporta exactamente como el modo 2, por lo que la función llamada no tiene que asumir la propiedad; sin embargo, insistiría en que si hay alguna duda sobre la transferencia de propiedad (en uso normal), el modo 2 debería preferirse al modo 3, de modo que el uso del modo 3 sea implícitamente una señal para las personas que llaman de que están renunciar a la propiedad. Se podría replicar que solo el paso de argumentos del modo 1 realmente señala la pérdida de propiedad forzada a las personas que llaman. Pero si un cliente tiene alguna duda sobre las intenciones de la función llamada, se supone que debe conocer las especificaciones de la función que se llama, lo que debería eliminar cualquier duda.

Es sorprendentemente difícil encontrar un ejemplo típico que involucre a nuestros list tipo que usa el paso de argumentos del modo 3. Mover una lista b al final de otra lista a es un ejemplo típico; sin embargo a (que sobrevive y mantiene el resultado de la operación) se pasa mejor usando el modo 2:

void append (list& a, list&& b)
 list* p=&a;
  while ((*p).get()!=nullptr) // find end of list a
    p=&(*p)->next;
  *p = std::move(b); // attach b; the variable b relinquishes ownership here

Un ejemplo puro de paso de argumentos en modo 3 es el siguiente que toma una lista (y su propiedad) y devuelve una lista que contiene los nodos idénticos en orden inverso.

list reversed (list&& l) noexcept // pilfering reversal of list
 list p(l.release()); // move list into temporary for traversal
  list result(nullptr);
  while (p.get()!=nullptr)
   // permute: result --> p->next --> p --> (cycle to result)
    result.swap(p->next);
    result.swap(p);
  
  return result;

Esta función podría llamarse como en l = reversed(std::move(l)); para invertir la lista en sí misma, pero la lista invertida también se puede usar de manera diferente.

Aquí el argumento se mueve inmediatamente a una variable local por eficiencia (se podría haber usado el parámetro l directamente en el lugar de p, pero luego acceder a él cada vez implicaría un nivel adicional de direccionamiento indirecto); por lo tanto, la diferencia con el paso de argumentos del modo 1 es mínima. De hecho, usando ese modo, el argumento podría haber servido directamente como variable local, evitando así ese movimiento inicial; esto es solo una instancia del principio general de que si un argumento pasado por referencia solo sirve para inicializar una variable local, uno podría pasarlo por valor en su lugar y usar el parámetro como variable local.

El uso del modo 3 parece estar respaldado por el estándar, como lo demuestra el hecho de que todas las funciones de biblioteca proporcionadas que transfieren la propiedad de los punteros inteligentes utilizando el modo 3. Un caso particular y convincente es el constructor. std::shared_ptr(auto_ptr&& p). Ese constructor usó (en std::tr1) para tomar un modificable lvalor referencia (al igual que el auto_ptr& copy constructor), y por lo tanto podría ser llamado con un auto_ptr lvalor p como en std::shared_ptr q(p), después de lo cual p se ha restablecido a nulo. Debido al cambio de modo 2 a 3 en el paso de argumentos, este código antiguo ahora debe reescribirse en std::shared_ptr q(std::move(p)) y luego continuará trabajando. Entiendo que al comité no le gustó el modo 2 aquí, pero tuvieron la opción de cambiar al modo 1, definiendo std::shared_ptr(auto_ptr p) en su lugar, podrían haberse asegurado de que el código antiguo funcione sin modificaciones, porque (a diferencia de los punteros únicos) los punteros automáticos pueden desreferenciarse silenciosamente a un valor (el objeto puntero en sí mismo se restablece a nulo en el proceso). Aparentemente, el comité prefirió defender el modo 3 sobre el modo 1, que eligieron romper activamente el código existente en lugar de utilizar el modo 1 incluso para un uso ya obsoleto.

Cuándo preferir el modo 3 sobre el modo 1

El modo 1 es perfectamente utilizable en muchos casos, y podría ser preferible al modo 3 en los casos en los que asumir la propiedad tomaría la forma de mover el puntero inteligente a una variable local como en el reversed ejemplo anterior. Sin embargo, puedo ver dos razones para preferir el modo 3 en el caso más general:

  • Es un poco más eficiente pasar un referencia que crear un puntero temporal y rechazar el viejo (manejar efectivo es algo laborioso); en algunos escenarios, el puntero puede pasar varias veces sin cambios a otra función antes de que sea realmente robado. Por lo general, tal aprobación requerirá la escritura. std::move (a menos que se use el modo 2), pero tenga en cuenta que este es solo un elenco que en realidad no hace nada (en particular, no elimina la referencia), por lo que tiene un costo cero adjunto.

  • ¿Debería ser concebible que algo arroje una excepción entre el inicio de la llamada a la función y el punto donde (o alguna llamada contenida) realmente mueve el objeto apuntado a otra estructura de datos (y esta excepción no está ya atrapada dentro de la propia función? ), cuando se utiliza el modo 1, el objeto al que hace referencia el puntero inteligente se destruirá antes de catch La cláusula puede manejar la excepción (porque el parámetro de función fue destruido durante el desenrollado de la pila), pero no así cuando se usa el modo 3. Este último le da al llamador la opción de recuperar los datos del objeto en tales casos (capturando la excepción). Tenga en cuenta que el modo 1 aquí no causa una fuga de memoria, pero puede conducir a una pérdida irrecuperable de datos para el programa, lo que también puede ser indeseable.

Devolver un puntero inteligente: siempre por valor

Para concluir unas palabras sobre regresando un puntero inteligente, presumiblemente apuntando a un objeto creado para ser utilizado por la persona que llama. Este no es realmente un caso comparable con pasar punteros a funciones, pero para completar me gustaría insistir en que en tales casos siempre regresa por valor (y no usestd::move en el return declaración). Nadie quiere conseguir un referencia a un puntero que probablemente acaba de ser rechazado.

Sí, tienes que hacerlo si tomas el unique_ptr por valor en el constructor. La explicidad es algo agradable. Ya que unique_ptr no se puede copiar (copia privada ctor), lo que escribió debería generar un error de compilación.

Si guardas alguna sospecha o forma de modernizar nuestro tutorial puedes escribir una crónica y con mucho placer lo leeremos.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *