c++ arguments c++11 unique-ptr

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



arguments c++11 (6)

Soy nuevo para mover la semántica en C ++ 11 y no sé muy bien cómo manejar los parámetros unique_ptr en constructores o funciones. Considere esta clase haciendo referencia a sí mismo:

#include <memory> class Base { public: typedef unique_ptr<Base> UPtr; Base(){} Base(Base::UPtr n):next(std::move(n)){} virtual ~Base(){} void setNext(Base::UPtr n) { next = std::move(n); } protected : Base::UPtr next; };

¿Es así como debería escribir funciones tomando argumentos unique_ptr ?

¿Y necesito usar std::move en el código de llamada?

Base::UPtr b1; Base::UPtr b2(new Base()); b1->setNext(b2); //should I write b1->setNext(std::move(b2)); instead?


Estas son las formas posibles de tomar un puntero único como argumento, así como su significado asociado.

(A) Por valor

Base(std::unique_ptr<Base> n) : next(std::move(n)) {}

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

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

Tomar un puntero único por valor significa que está transfiriendo la propiedad del puntero a la función / objeto / etc en cuestión. Después de construir nextBase , se garantiza que nextBase esté vacío . No eres dueño 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 realidad no mueve nada; Es sólo un elenco de lujo. std::move(nextBase) devuelve una Base&& que es una referencia de valor r para nextBase . Eso es todo lo que hace.

Debido a que Base::Base(std::unique_ptr<Base> 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<Base> desde la Base&& que le dimos a la función a través de std::move(nextBase) . Es la construcción de este temporal que realmente mueve el valor de nextBase al argumento de función n .

(B) Por referencia de valor l no const.

Base(std::unique_ptr<Base> &n) : next(std::move(n)) {}

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

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

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

Base newBase(nextBase);

No hay garantía de que nextBase esté vacío. Puede estar vacío; puede que no Realmente depende de lo que Base::Base(std::unique_ptr<Base> &n) quiere hacer. Debido a 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 constante de valor l

Base(std::unique_ptr<Base> const &n);

No muestro una implementación, porque no puedes moverte de un const& . Al pasar una const& , está diciendo que la función puede acceder a la Base través del puntero, pero no puede almacenarla en ninguna parte. No puede reclamar la propiedad de ella.

Esto puede ser útil. No necesariamente para su caso específico, pero siempre es bueno poder entregarle un puntero a alguien y saber que no pueden (sin romper las reglas de C ++, como no tener que deshacerse de const ) ser propietario de él. Ellos no pueden almacenarlo. Pueden pasarlo a otros, pero esos otros deben cumplir con las mismas reglas.

(D) Por referencia de valor r

Base(std::unique_ptr<Base> &&n) : next(std::move(n)) {}

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

  1. Puede pasar un temporal:

    Base newBase(std::unique_ptr<Base>(new Base)); //legal now..

  2. Debe usar std::move cuando pase argumentos no temporales.

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

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

Tiene una expectativa razonable de que, una vez completada esta línea, nextBase esté vacía. Debería haber sido movido de Después de todo, tienes ese std::move sentado allí, diciéndote que ha ocurrido un movimiento.

El problema es que no tiene. No se garantiza que haya sido movido de. Puede haberse movido de, pero solo lo sabrás mirando el código fuente. No se puede decir sólo de la firma de función.

Recomendaciones

  • (A) Por valor: si quiere decir que una función reclama la propiedad de unique_ptr , unique_ptr por valor.
  • (C) Por referencia de valor l const: Si quiere que una función use simplemente el unique_ptr durante la ejecución de esa función, unique_ptr por const& . Alternativamente, pase un & o const& al tipo real al que se apunta, en lugar de usar 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 internas), && por && . Pero recomiendo encarecidamente no hacer esto siempre que sea posible.

Cómo manipular unique_ptr

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

Si tomas un unique_ptr por valor, puedes moverte de él libremente. Pero el movimiento no ocurre realmente debido a std::move . Tome la siguiente declaración:

std::unique_ptr<Base> newPtr(std::move(oldPtr));

Esto es realmente dos declaraciones:

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

(Nota: el código anterior no se compila técnicamente, ya que las referencias de valores r no temporales no son realmente valores r. Está aquí solo para fines de demostración).

El temporary es solo una referencia de valor r a oldPtr . Es en el constructor de newPtr donde ocurre el movimiento. El constructor de movimientos de unique_ptr (un constructor que toma un && a sí mismo) es lo que hace el movimiento real.

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


Permítame tratar de establecer los diferentes modos viables de pasar punteros a objetos cuya memoria es administrada por una instancia de la plantilla de clase std::unique_ptr ; también se aplica a la plantilla de clase std::auto_ptr más antigua (que creo que permite todos los usos que hace ese puntero único, pero para los que además se aceptarán valores lvalizables donde se esperan valores, sin tener que invocar std::move ), y hasta cierto punto 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<node> list; struct node { int entry; list next; }

Las instancias de dicha lista (a las que no se les puede permitir compartir partes con otras instancias o ser circulares) son propiedad de quien posee el puntero de la list inicial. Si el código del cliente sabe que la lista que almacena nunca estará vacía, también puede optar por almacenar el primer node directamente en lugar de una list . No es necesario definir un destructor para el node : dado que los destructores para sus campos se llaman automáticamente, el puntero inteligente eliminará la lista completa 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 para datos simples. También las funciones en sí mismas proporcionan ocasionalmente (recursivamente) un ejemplo de código de cliente también. Por supuesto, el typedef para la list está unique_ptr hacia unique_ptr , pero la definición podría cambiarse para usar auto_ptr o shared_ptr en shared_ptr lugar sin necesidad de cambiar mucho a lo que se dice a continuación (en particular, la seguridad de las excepciones está garantizada sin la necesidad de escribir destructores).

Modos de pasar punteros inteligentes alrededor

Modo 0: pase un puntero o un 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 este caso, su función no necesita preocuparse de quién es el propietario del objeto apuntado o por qué medios se maneja la propiedad, por lo que pasar un puntero en bruto es perfectamente seguro y la forma más flexible, ya que, independientemente de la propiedad, un cliente siempre puede produce un puntero en bruto (ya sea llamando al método de get o desde la dirección del operador & ).

Por ejemplo, la función para calcular la longitud de dicha lista, no se debe dar un argumento de list , sino un puntero en bruto:

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

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

Si se garantiza que el puntero no sea nulo (lo que no es el caso aquí ya que las listas pueden estar vacías), es posible que prefiera pasar una referencia en lugar de un puntero. Puede ser un puntero / referencia a no const si la función necesita actualizar el contenido del (de los) nodo (s), sin agregar o eliminar ninguno de ellos (este último implica la propiedad).

Un caso interesante que cae en la categoría de modo 0 es hacer una copia (profunda) de la lista; mientras que una función que haga esto debe, por supuesto, transferir la propiedad de la copia que crea, no tiene que ver con la propiedad de la lista que está copiando. Por lo que podría definirse como sigue:

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

Este código merece una mirada de cerca, tanto para la pregunta de por qué se compila (el resultado de la llamada recursiva para copy en la lista de inicializadores se enlaza con el argumento de referencia de valor en el constructor de movimiento de unique_ptr<node> , también conocido como list , cuando se inicializa el next campo del node generado), y para la pregunta de por qué es seguro para las excepciones (si durante el proceso de asignación recursivo se agota la memoria y se std::bad_alloc alguna llamada de new lanzamientos std::bad_alloc , en ese momento un puntero a la lista parcialmente construida se mantiene anónimamente en una list temporal de tipo creada para la lista inicializadora, y su destructor limpiará esa lista parcial). Por cierto, uno debería resistir la tentación de reemplazar (como lo hice inicialmente) el segundo valor nullptr por p , que después de todo se sabe que es nulo en ese punto: no se puede construir un puntero inteligente desde un puntero (sin procesar) hasta una 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 al que se apunta de inmediato: el puntero inteligente que sostuvo el llamante (ya sea en una variable con nombre o un temporario anónimo) se copia en el valor de argumento en la entrada de la función y el llamador el puntero se ha vuelto nulo (en el caso de una copia temporal, la copia podría haber sido borrada, pero en cualquier caso la persona que llama ha perdido el acceso al apuntado al objeto). Me gustaría llamar a este modo de llamada en efectivo : la persona que llama paga por adelantado por el servicio llamado, y no puede hacerse ilusiones sobre la propiedad después de la llamada. Para aclarar esto, las reglas de 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 lvalor); en este caso (pero no para el modo 3 a continuación), esta función hace lo que su nombre sugiere, es decir, mover el valor de la variable a un temporal, dejando la variable nula.

Para los casos en que la función llamada toma posesión incondicional de (apunta) el objeto apuntado a, este modo utilizado con std::unique_ptr o std::auto_ptr es una buena manera de pasar un puntero junto con su propiedad, lo que evita cualquier riesgo de pérdidas de memoria. No obstante, creo que hay muy pocas situaciones en las que el modo 3 a continuación no se prefiera (aunque sea un poco) sobre el modo 1. Por esta razón, no proporcionaré ejemplos de uso de este modo. (Pero vea el ejemplo reversed del modo 3 a continuación, donde se observa que el modo 1 también funcionaría al menos). Si la función toma más argumentos que solo este puntero, puede suceder que además haya una razón técnica para evitar modo 1 (con std::unique_ptr o std::auto_ptr ): dado que una operación de movimiento real tiene lugar mientras pasa una variable de puntero p por la expresión std::move(p) , no se puede suponer que p tiene un valor útil mientras evaluar los otros argumentos (el orden de evaluación no se especifica), lo que podría conducir a errores sutiles; por el contrario, el uso del modo 3 garantiza que no se mueva desde p 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, ya que con una sola función de definición, le permite al llamante elegir si desea conservar una copia compartida del puntero mientras crea una nueva copia compartida para que la utilice la función (esto sucede cuando se proporciona un argumento lvalue; el constructor de copia para los punteros compartidos utilizados en la llamada aumenta el recuento de referencia), o simplemente le da a la función una copia del puntero sin retener uno ni tocar el recuento de referencia (esto sucede cuando un argumento de valor r se proporciona, posiblemente un lvalue envuelto en una llamada de std::move ). Por ejemplo

void f(std::shared_ptr<X> x) // call by shared cash { container.insert(std::move(x)); } // store shared pointer in container void client() { std::shared_ptr<X> p = std::make_shared<X>(args); f(p); // lvalue argument; store pointer in container but keep a copy f(std::make_shared<X>(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>& x) (para el caso de lvalue) y void f(std::shared_ptr<X>&& x) (para el caso de rvalue), con los cuerpos de función difieren solo en que la primera versión invoca la semántica de copia (utilizando la construcción / asignación de copia cuando usa x ), pero la segunda versión mueve la semántica (escribiendo std::move(x) lugar, como en el código de ejemplo). Por lo tanto, para los punteros compartidos, el modo 1 puede ser útil para evitar la duplicación de código.

Modo 2: pase un puntero inteligente por referencia de valor (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 ella. Me gustaría llamar a este método llamada por tarjeta : la persona que llama garantiza el pago al dar un número de tarjeta de crédito. La referencia se puede usar para tomar posesión del objeto apuntado, pero no tiene que hacerlo. 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 argumento. Un llamante con una expresión de valor que desea pasar a tal función se vería obligado a almacenarlo en una variable con nombre para poder realizar la llamada, ya que el lenguaje solo proporciona una conversión implícita a una referencia de valor constante (en referencia a una ) a partir de un valor. (A diferencia de la situación opuesta manejada por std::move , no es posible realizar una conversión de Y&& a Y& , con Y el tipo de puntero inteligente; no obstante, esta conversión podría obtenerse mediante una función de plantilla simple si realmente se desea; consulte https://.com/a/24868376/1436796 ). En el caso de que la función llamada intente tomar posesión incondicional del objeto, robando el argumento, la obligación de proporcionar un argumento de valor l es dar una señal errónea: la variable no tendrá un 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 las personas que llaman que proporcionen un valor, debe ser preferido para tal uso.

Sin embargo, hay un caso de uso válido para el modo 2, es decir, funciones que pueden modificar el puntero o el objeto apuntado de una manera que involucra la propiedad . Por ejemplo, una función que prefija un nodo a una list proporciona un ejemplo de tal uso:

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

Claramente, sería indeseable aquí forzar 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 es diferente a la anterior.

Nuevamente, es interesante observar qué sucede si falla la llamada prepend por falta de memoria libre. Entonces la new llamada lanzará std::bad_alloc ; en este momento, ya que no se pudo asignar ningún node , es cierto que la referencia rvalue pasada (modo 3) de std::move(l) aún no se puede haber robado, como se haría para construir el next campo de El node que no pudo ser asignado. Por lo tanto, el puntero inteligente original l todavía contiene la lista original cuando se produce el error; esa lista será bien destruida por el puntero destructor inteligente, o en caso de que sobreviva gracias a una cláusula de catch suficientemente temprana, aún conservará 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 hay:

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); }

Nuevamente la corrección es bastante sutil aquí. En particular, en la declaración final, el puntero (*p)->next mantiene dentro del nodo que se va a eliminar está desvinculado (por release , que devuelve el puntero pero hace que el original sea nulo) antes de que se reset (implícitamente) el nodo (cuando se destruye) el antiguo valor mantenido por p ), asegurando que uno y solo un nodo se destruya en ese momento. (En la forma alternativa mencionada en el comentario, este tiempo se dejaría a los std::unique_ptr internos de la implementación del operador de asignación de movimientos de la list instancias std::unique_ptr ; la norma dice 20.7.1.2.3; 2 que este operador debería actúe "como si fuera a llamar reset(u.release()) ", donde el tiempo también debería ser seguro aquí.)

Tenga en cuenta que los clientes que almacenan una variable de node local para una lista siempre vacía no pueden llamar a remove_first y remove_first , y con razón ya que las implementaciones dadas no podrían funcionar en tales casos.

Modo 3: pase un puntero inteligente por referencia de valor (modificable)

Este es el modo preferido para usar cuando simplemente se toma posesión del puntero. Me gustaría llamar a este método llamada por cheque : la persona que llama debe aceptar renunciar a la propiedad, como si proporcionara efectivo, firmando el cheque, pero el retiro real se pospone hasta que la función llamada realmente pise 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 lime (si es un valor r, la parte de "renunciar a la propiedad" es obvia y no requiere 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 existe alguna duda sobre la transferencia de propiedad (en el uso normal), debería preferirse el modo 2 al modo 3, de modo que el uso del modo 3 es una señal implícita para los llamantes de que están renunciando a la propiedad. Uno podría replicar que solo el paso del argumento del modo 1 realmente indica una pérdida forzada de propiedad para quienes 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 a la que se llama, lo que debería eliminar cualquier duda.

Es sorprendentemente difícil encontrar un ejemplo típico que involucre nuestro tipo de list que use 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 del paso del argumento del 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 revertir la lista a sí misma, pero la lista revertida también se puede utilizar de manera diferente.

Aquí, el argumento se mueve inmediatamente a una variable local por eficiencia (uno podría haber usado el parámetro l directamente en lugar de p , pero acceder a él cada vez implicaría un nivel adicional de direccionamiento indirecto); por lo tanto, la diferencia con el paso del argumento del modo 1 es mínima. De hecho, al usar 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 ser recomendado 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 mediante el modo 3. Un caso convincente particular en este punto es el constructor std::shared_ptr<T>(auto_ptr<T>&& p) . Ese constructor usó (en std::tr1 ) para tomar una referencia de std::tr1 modificable (como el auto_ptr<T>& copy constructor), y por lo tanto podría llamarse con un auto_ptr<T> lvalue p como en std::shared_ptr<T> q(p) , después de lo cual p se ha restablecido a nulo. Debido al cambio del modo 2 al 3 en el paso de argumentos, este código antiguo ahora debe reescribirse en std::shared_ptr<T> q(std::move(p)) y luego continuará trabajando. Entiendo que al comité no le gustó el modo 2 aquí, pero tenían la opción de cambiar al modo 1, definiendo std::shared_ptr<T>(auto_ptr<T> p) lugar, podrían haberse asegurado de que el código antiguo funcione sin modificación, porque (a diferencia de los punteros únicos) los punteros automáticos pueden ser desreferenciados silenciosamente a un valor (el objeto del puntero se restablece a nulo en el proceso). Al parecer, el comité tanto prefería defender el modo 3 en lugar del modo 1, que eligieron romper activamente el código existente en lugar de usar 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 preferirse al modo 3 en los casos en los que asumir que la propiedad tomaría la forma de mover el puntero inteligente a una variable local como en el 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 una referencia que crear un temporal y desviar el puntero antiguo (manejar dinero es algo laborioso); en algunos casos, el puntero puede pasarse varias veces sin cambiar a otra función antes de que realmente sea robado. Dicha aprobación generalmente requerirá la escritura de std::move (a menos que se use el modo 2), pero tenga en cuenta que esto es solo una conversión que en realidad no hace nada (en particular, sin referencia), por lo que no tiene costo.

  • ¿Debería ser concebible que algo arroje una excepción entre el inicio de la llamada a la función y el punto en el que (o alguna llamada contenida) realmente mueve el objeto apuntado a otra estructura de datos (y esta excepción no se encuentra dentro de la propia función) ), luego, cuando se usa el modo 1, el objeto al que hace referencia el puntero inteligente se destruirá antes de que una cláusula catch pueda manejar la excepción (porque el parámetro de la función se destruyó durante el desenrollado de la pila), pero no así cuando se usa el modo 3. Este último proporciona el llamante tiene la opción de recuperar los datos del objeto en tales casos (detectando la excepción). Tenga en cuenta que el modo 1 aquí no causa una pérdida de memoria , pero puede ocasionar una pérdida de datos no recuperable para el programa, lo que también puede ser indeseable.

Devolviendo un puntero inteligente: siempre por valor

Para concluir una palabra acerca de devolver un puntero inteligente, presumiblemente apunta a un objeto creado para ser utilizado por el llamante. Este no es realmente un caso comparable con el paso de punteros a funciones, pero para completar, me gustaría insistir en que en tales casos siempre se devuelve por valor (y no use std::move en la declaración de return ). Nadie quiere obtener una referencia a un puntero que probablemente haya sido rechazado.


Sí, debes hacerlo si tomas el unique_ptr por valor en el constructor. La simplicidad es algo bueno. Dado que unique_ptr se puede copiar (ctor de copia privada), lo que escribiste debería darte un error de compilación.


Edición: esta respuesta es incorrecta, aunque, estrictamente hablando, el código funciona. Solo lo dejo aquí porque la discusión bajo él es demasiado útil. Esta otra respuesta es la mejor respuesta en el momento en que edité esto por última vez: ¿Cómo le paso un argumento unique_ptr a un constructor o una función?

La idea básica de ::std::move es que las personas que le pasan el unique_ptr deben usarlo para expresar el conocimiento de que saben que el unique_ptr que están pasando perderán la propiedad.

Esto significa que debe usar una referencia rvalue para un unique_ptr en sus métodos, no un unique_ptr sí. Esto no funcionará de todos modos porque pasar un unique_ptr requeriría hacer una copia, y eso está explícitamente prohibido en la interfaz para unique_ptr . Curiosamente, el uso de una referencia rvalue denominada lo vuelve a convertir en un lvalue, por lo que también debe usar ::std::move dentro de sus métodos.

Esto significa que sus dos métodos deberían verse así:

Base(Base::UPtr &&n) : next(::std::move(n)) {} // Spaces for readability void setNext(Base::UPtr &&n) { next = ::std::move(n); }

Entonces las personas que usan los métodos harían esto:

Base::UPtr objptr{ new Base; } Base::UPtr objptr2{ new Base; } Base fred(::std::move(objptr)); // objptr now loses ownership fred.setNext(::std::move(objptr2)); // objptr2 now loses ownership

Como puede ver, el ::std::move expresa que el puntero va a perder la propiedad en el punto donde es más importante y útil saberlo. Si esto ocurriera de manera invisible, sería muy confuso para las personas que usan su clase que objptr pierda la propiedad repentinamente sin una razón aparente.


A la respuesta más votada. Prefiero pasar por referencia de valor.

Entiendo cuál es el problema que puede tener el hecho de pasar por una referencia rvalue. Pero dividamos este problema en dos lados:

  • para la persona que llama:

Debo escribir el código Base newBase(std::move(<lvalue>))o Base newBase(<rvalue>).

  • para la persona llamada:

El autor de la biblioteca debe garantizar que realmente moverá el unique_ptr para inicializar el miembro si quiere poseer la propiedad.

Eso es todo.

Si pasa por referencia rvalue, solo invocará una instrucción de "mover", pero si pasa por valor, son dos.

Sí, si el autor de la biblioteca no es experto en esto, no puede mover unique_ptr para inicializar el miembro, pero es el problema del autor, no de usted. Pase lo que pase por valor o por referencia de valor, su código es el mismo!

Si está escribiendo una biblioteca, ahora sabe que debe garantizarla, así que simplemente hágalo, pasar una referencia rvalue es una mejor opción que valor. El cliente que usa tu biblioteca simplemente escribirá el mismo código.

Ahora, por tu pregunta. ¿Cómo paso un argumento unique_ptr a un constructor o una función?

Sabes cual es la mejor opción

http://scottmeyers.blogspot.com/2014/07/should-move-only-types-ever-be-passed.html


Base(Base::UPtr n):next(std::move(n)) {}

debería ser mucho mejor como

Base(Base::UPtr&& n):next(std::forward<Base::UPtr>(n)) {}

y

void setNext(Base::UPtr n)

debiera ser

void setNext(Base::UPtr&& n)

con el mismo cuerpo.

Y ... ¿qué es evt en handle() ?