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

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  class Base { public: typedef unique_ptr 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 las 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 n) : next(std::move(n)) {} 

Para que el usuario lo llame, debe hacer una de las siguientes cosas:

 Base newBase(std::move(nextBase)); Base fromTemp(std::unique_ptr(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 estará vacío . No posee el objeto, y ni siquiera tiene un puntero a él. Se fue.

Esto está garantizado porque tomamos el parámetro por valor. std::move realidad no mueve nada; es solo un elenco elegante. std::move(nextBase) devuelve una Base&& que es una referencia de valor r para nextBase . Eso es todo lo que hace.

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

(B) Por referencia de valor l sin const

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

Esto debe invocarse en un valor l real (una variable nombrada). 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 const: 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 &n) quiera hacer. Debido a eso, no es muy evidente solo a partir de la función de la firma lo que va a suceder; tienes que leer la implementación (o la documentación asociada).

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

(C) Por referencia const l-value

 Base(std::unique_ptr const &n); 

No muestro una implementación, porque no se puede mover de un const& . Al pasar un const& , estás diciendo que la función puede acceder a la Base través del puntero, pero no puede almacenarla en ningún lado. No puede reclamar su propiedad.

Esto puede ser útil. No necesariamente para su caso específico, pero siempre es bueno poder darle un puntero a alguien y saber que no puede (sin romper las reglas de C ++, como no eliminar const ) reclamar su propiedad. No pueden almacenarlo. Pueden pasárselo a otros, pero esos otros tienen que cumplir con 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 no consistente”. Las diferencias son dos cosas.

  1. Puedes pasar un temporal:

     Base newBase(std::unique_ptr(new Base)); //legal now.. 
  2. Debe usar std::move al pasar 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, después de que esta línea se complete, nextBase debe estar vacía. Debería haberse movido de. Después de todo, tienes ese std::move sentado allí, diciéndote que el movimiento ha ocurrido.

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

Recomendaciones

  • (A) Por valor: si quiere decir que una función reclama la propiedad de un unique_ptr , unique_ptr por valor.
  • (C) Por referencia const l-value: si quiere decir que una función simplemente usa el unique_ptr mientras dura la ejecución de esa función, unique_ptr por const& . Alternativamente, pase un & const& al tipo real al que 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 del código interno), entonces tómala por && . Pero le recomiendo que no lo haga 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 libremente. Pero el movimiento en realidad no ocurre debido a std::move . Tome la siguiente statement:

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

Esto es realmente dos declaraciones:

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

(nota: el código anterior no se comstack técnicamente, ya que las referencias de valor r no temporales no son en realidad 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 movimiento de unique_ptr (un constructor que toma un && para sí mismo) es lo que hace el movimiento real.

Si tiene un valor de unique_ptr y desea almacenarlo en alguna parte, debe usar std::move para hacer el almacenamiento.

Permítanme intentar establecer los diferentes modos viables de pasar punteros a objetos cuya memoria está gestionada por una instancia de la plantilla de clase std::unique_ptr ; también se aplica a la plantilla de la clase std::auto_ptr más antigua (que creo que permite todos los usos que hace el puntero único, pero que además se aceptarán valores l modificables donde se esperan valores r, sin tener que invocar a 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 list; struct node { int entry; list next; } 

Las instancias de dicha lista (que no pueden compartir partes con otras instancias o ser circulares) son propiedad de quien tenga 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 elegir almacenar el primer node directamente en lugar de una list . No es necesario definir un destructor para el node : dado que los destructores de sus campos se invocan automáticamente, toda la lista será eliminada recursivamente por el destructor del puntero inteligente una vez que finalice la vida útil del puntero o nodo inicial.

Este tipo recursivo brinda la oportunidad de analizar algunos casos que son menos visibles en el caso de un puntero inteligente a datos simples. También las funciones en sí mismas ocasionalmente proveen (recursivamente) un ejemplo de código de cliente también. El typedef for list está, por supuesto, sesgado hacia unique_ptr , pero la definición podría cambiarse para usar auto_ptr o shared_ptr en shared_ptr lugar sin mucha necesidad de cambiar a lo que se dice a continuación (especialmente con respecto a la seguridad de excepciones sin la necesidad de escribir destructores).

Modos de pasar punteros inteligentes alrededor

Modo 0: pasar 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 lo haga tomar un puntero inteligente en absoluto. En este caso, su función no necesita preocuparse de quién es el dueño del objeto apuntado, o de qué manera se administra esa propiedad, por lo que pasar un puntero sin procesar 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 método get o desde el operador de dirección & ).

Por ejemplo, la función para calcular la longitud de dicha lista, no debe dar un argumento de list , sino 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 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 es nulo (que no es el caso aquí, ya que las listas pueden estar vacías), uno podría preferir pasar una referencia en lugar de un puntero. Puede ser un puntero / referencia a no const si la función necesita actualizar el contenido de los nodos, sin agregar o eliminar ninguno de ellos (este último implicaría 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 hace esto debe transferir la propiedad de la copia que crea, no se trata de la propiedad de la lista que está copiando. Por lo tanto, se podría definir de la siguiente manera:

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

Este código merece una mirada cercana, tanto para la pregunta de por qué se comstack en absoluto (el resultado de la llamada recursiva para copy en la lista de inicializadores se une al argumento de referencia rvalue en el constructor de movimientos de unique_ptr , aka list , al inicializar el next campo del node generado), y para la pregunta de por qué es excepcionalmente seguro (si durante el proceso recursivo de asignación se agota la memoria y alguna llamada de new throws std::bad_alloc , entonces en ese momento un puntero a la lista parcialmente construida se mantiene de forma anónima en una list temporal de tipos creada para la lista de inicializadores, y su destructor limpiará esa lista parcial). Por cierto, uno debe resistir la tentación de reemplazar (como lo hice inicialmente) el segundo nullptr por p , que después de todo se sabe que es nulo en ese punto: uno no puede construir un puntero inteligente desde un puntero (en bruto) 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 señalado de inmediato: el puntero inteligente que sostuvo la persona que llama (ya sea en una variable nombrada o temporal anónima) se copia en el valor del argumento en la entrada de la función y el puntero se ha convertido en nulo (en el caso de un temporal, la copia puede haber sido eliminada, pero en cualquier caso el llamante ha perdido el acceso al objeto apuntado). Me gustaría llamar a este modo 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 dejar esto en claro, las reglas de lenguaje requieren que quien 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 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 se apropia de manera incondicional del objeto apuntado (pilfers), este modo utilizado 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érdidas de memoria. No obstante, creo que hay muy pocas situaciones en las que el modo 3 a continuación no se prefiera (muy ligeramente) al 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 señala que el modo 1 también lo haría). Si la función requiere más argumentos que solo este puntero, puede ocurrir que exista además un motivo técnico para evitarlo. modo 1 (con std::unique_ptr o std::auto_ptr ): dado que una operación de movimiento real tiene lugar al pasar una variable de puntero p por la expresión std::move(p) , no puede suponerse que p tenga un valor útil mientras evaluar los otros argumentos (el orden de evaluación no especificado), lo que podría conducir a errores sutiles; por el contrario, el uso del modo 3 asegura que no se produce ningún movimiento 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 definición de función permite al llamante elegir si desea mantener una copia compartida del puntero por sí mismo al crear 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 utilizado 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 referencias (esto ocurre cuando un argumento rvalue se proporciona, 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 lvalue) y void f(std::shared_ptr&& x) (para el caso rvalue), con los cuerpos de función difieren solo en que la primera versión invoca semántica de copia (usando la construcción / asignación de copia cuando se usa x ), pero la segunda versión mueve la semántica (escribiendo std::move(x) , 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: pasar un puntero inteligente por referencia (validable) lvalue

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 : el que llama se asegura de pagar dando un número de tarjeta de crédito. La referencia se puede usar para apropiarse del objeto apuntado, pero no tiene que ser así. 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 rvalue que desea pasar a dicha función se vería obligado a almacenarlo en una variable con nombre para poder hacer la llamada, ya que el lenguaje solo proporciona una conversión implícita a una referencia de valor constante (refiriéndose a un ) de un valor r. (A diferencia de la situación opuesta manejada por std::move , un molde de Y&& a Y& , con Y el tipo de puntero inteligente, no es posible; sin embargo, esta conversión podría obtenerse mediante una simple función de plantilla, si realmente se desea, ver https: // stackoverflow.com/a/24868376/1436796 ). Para el caso en que la función llamada tiene la intención de apropiarse incondicionalmente del objeto, robando del argumento, la obligación de proporcionar un argumento lvalue está dando la señal equivocada: la variable no tendrá 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 r, debe ser el preferido para dicho 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 implica la propiedad . Por ejemplo, una función que prefija un nodo a una list proporciona un ejemplo de dicho uso:

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

Claramente, 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.

De nuevo, es interesante observar lo que sucede si falla la llamada de prepend por falta de memoria libre. Entonces la new llamada lanzará std::bad_alloc ; en este punto en el tiempo, dado que no se podría asignar ningún node , es cierto que la referencia rvalue pasada (modo 3) de std::move(l) no puede haberse robado aún, ya que eso se haría para construir el next campo de el node que no pudo ser asignado. Por lo tanto, el puntero inteligente original l conserva la lista original cuando se produce el error; esa lista será destruida apropiadamente por el destructor del puntero inteligente, o en caso de que l sobreviva gracias a una cláusula de catch suficientemente temprana, aún mantendrá la lista original.

Ese fue un ejemplo constructivo; Con un guiño a esta pregunta, uno también 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); } 

De nuevo, la corrección es bastante sutil aquí. Notablemente, en la statement final, el puntero (*p)->next contenido 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 reset (implícitamente) destruye ese nodo (cuando destruye el valor anterior mantenido por p ), asegurando que uno y solo un nodo se destruye en ese momento. (En la forma alternativa mencionada en el comentario, este tiempo se dejaría a las std::unique_ptr internas de la implementación del operador de asignación de movimiento 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 llamara a reset(u.release()) “, donde el tiempo debería ser seguro aquí también.

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

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

Este es el modo preferido para usar cuando simplemente asumes la propiedad del puntero. Me gustaría llamar a este método por cheque : la persona que llama debe aceptar renunciar a la propiedad, como si proporcionara dinero en efectivo, al firmar el cheque, pero el retiro real se pospone hasta que la función llamada realmente robe el puntero (exactamente como lo haría al usar el modo 2 ) La “firma del cheque” concretamente significa que las personas que llaman deben envolver un argumento en std::move (como en el modo 1) si es un lvalue (si es un valor r, la parte de “renunciar a la propiedad” es obvia y no requiere código por 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 incertidumbre 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 los llamadores de que están renunciando a la propiedad. Uno podría replicar que solo el argumento del modo 1 que pasa realmente señala la pérdida forzada de la propiedad de los llamantes. Pero si un cliente tiene dudas 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 implique nuestro tipo de list que utiliza el paso de argumento del modo 3. Mover una lista b al final de otra lista a es un ejemplo típico; sin embargo, a (que sobrevive y contiene el resultado de la operación) se supera mejor utilizando 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 de 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 l = reversed(std::move(l)); para invertir la lista en sí mismo, pero la lista invertida también se puede usar de manera diferente.

Aquí el argumento se mueve inmediatamente a una variable local para la 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 extra de indirección); de ahí que la diferencia con el paso del argumento del modo 1 sea 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, también podría pasarlo por valor y usar el parámetro como variable local.

El uso del modo 3 parece ser defendido por el estándar, como lo demuestra el hecho de que todas las funciones de biblioteca proporcionadas transfieren la propiedad de punteros inteligentes utilizando el modo 3. Un caso particular convincente es el constructor std::shared_ptr(auto_ptr&& p) . Ese constructor usó (en std::tr1 ) para tomar una referencia de std::tr1 modificable (al igual que el constructor auto_ptr& copy), y por lo tanto podría llamarse con un auto_ptr lvalue p como en std::shared_ptr q(p) , después de lo cual p se ha restablecido a nulo. Debido al cambio del modo 2 al 3 en la aprobación de los argumentos, este código antiguo ahora debe reescribirse a std::shared_ptr q(std::move(p)) y luego continuará funcionando. Entiendo que al comité no le gustaba el modo 2 aquí, pero tenían la opción de cambiar al modo 1, definiendo std::shared_ptr(auto_ptr p) , podrían haber asegurado que el código anterior funciona sin modificación, porque (a diferencia de los punteros únicos) los auto-punteros pueden desreferenciarse silenciosamente a un valor (el objeto del puntero mismo se restablece a nulo en el proceso). Aparentemente, el comité prefirió abogar por el modo 3 sobre el modo 1, que eligieron romper activamente el código existente en lugar de usar el modo 1 incluso para un uso ya desaprobado.

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 que asumir 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 puntero temporal y el puntero anterior (manejar efectivo es algo laborioso); en algunos escenarios, el puntero puede pasarse varias veces sin cambios a otra función antes de que sea robado. Tal aprobación generalmente requerirá escribir std::move (a menos que se use el modo 2), pero tenga en cuenta que esto es solo un molde que en realidad no hace nada (en particular, no hay desreferencia), por lo que tiene cero costo 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) mueve realmente el objeto apuntado a otra estructura de datos (y esta excepción ya no está atrapada dentro de la función misma) ), cuando se usa el modo 1, el objeto referido por el puntero inteligente será destruido antes de que una cláusula de catch pueda manejar la excepción (porque el parámetro de función fue destruido durante el desenrollamiento de la stack), pero no cuando se usa el modo 3. Este último da el que llama 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 conducir a una pérdida irrecuperable de datos para el progtwig, lo que también podría ser indeseable.

Devolver un puntero inteligente: siempre por valor

Para concluir una palabra acerca de devolver un puntero inteligente, presumiblemente apuntando a un objeto creado para que lo use 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 devuelva por valor (y no utilice std::move en la statement de return ). Nadie quiere obtener una referencia a un puntero que probablemente acaba de ser rechazado.

Sí, tienes que hacerlo si tomas unique_ptr por valor en el constructor. La explicidad es algo bueno. Como unique_ptr se puede copiar (copia privada), lo que usted escribió debería darle un error de comstackción.

Editar: Esta respuesta es incorrecta, aunque, estrictamente hablando, el código funciona. Solo lo dejo aquí porque la discusión debajo es demasiado útil. Esta otra respuesta es la mejor respuesta dada en el momento de la última edición de esto: ¿Cómo 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 unique_ptr el unique_ptr deberían usarlo para express el conocimiento de que saben que el unique_ptr que están unique_ptr perderá la propiedad.

Esto significa que debe usar una referencia rvalue a un unique_ptr en sus métodos, no un unique_ptr sí mismo. Esto no funcionará de todos modos porque pasar un antiguo 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 con nombre lo convierte de nuevo en un lvalue, por lo que debe usar ::std::move dentro de sus métodos también.

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 relevante y útil saberlo. Si esto sucediera de manera invisible, sería muy confuso para las personas que usan su clase tener objptr repente perder la propiedad sin razón aparente.

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

debería ser mucho mejor como

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

y

 void setNext(Base::UPtr n) 

debiera ser

 void setNext(Base::UPtr&& n) 

con el mismo cuerpo

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

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

Entiendo cuál es el problema sobre pasar por referencia de valor r puede causar. Pero dividamos este problema en dos lados:

  • para quien llama:

Debo escribir el código Base newBase(std::move()) o Base newBase() .

  • para el llamado:

Library author should guarantee it will actually move the unique_ptr to initialize member if it want own the ownership.

Eso es todo.

If you pass by rvalue reference, it will only invoke one “move” instruction, but if pass by value, it’s two.

Yep, if library author is not expert about this, he may not move unique_ptr to initialize member, but it’s the problem of author, not you. Whatever it pass by value or rvalue reference, your code is same!

If you are writing a library, now you know you should guarantee it, so just do it, passing by rvalue reference is a better choice than value. Client who use you library will just write same code.

Now, for your question. How do I pass a unique_ptr argument to a constructor or a function?

You know what’s the best choice.

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