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


400

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

#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 debo escribir funciones tomando unique_ptrargumentos?

¿Y debo usarlo std::moveen 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?


1
¿No es una falla de segmentación cuando estás llamando a b1-> setNext en un puntero vacío?
balki

Respuestas:


836

Estas son las posibles formas 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 que newBasese construye, nextBasese garantiza que esté vacío . No eres el propietario del objeto y ya ni siquiera tienes un puntero. Se fue.

Esto está garantizado porque tomamos el parámetro por valor. std::moveen realidad no mueve nada; Es solo un elenco elegante. std::move(nextBase)devuelve un Base&&que es una referencia de valor r 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 a std::unique_ptr<Base>partir del Base&&cual le dimos la función std::move(nextBase). Es la construcción de este temporal lo que realmente mueve el valor nextBaseal argumento de la función n.

(B) Por referencia de valor l no constante

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

Esto se debe invocar 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 nextBaseesté vacío. Se puede estar vacía; Puede que no. Realmente depende de lo que Base::Base(std::unique_ptr<Base> &n)quiera 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 la documentación asociada).

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

(C) Por referencia de valor l constante

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

No muestro una implementación, porque no puedes pasar de a const&. Al pasar a const&, está diciendo que la función puede acceder a Basetravé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 descartarlo const) reclamar la propiedad del mismo. 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 constante". Las diferencias son dos cosas.

  1. Usted puede pasar una temporal:

    Base newBase(std::unique_ptr<Base>(new Base)); //legal now..
  2. Usted debe utilizar std::moveal pasar argumentos que no sean 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, nextBasedebería estar vacía. Debería haber sido movido de. Después de todo, tienes eso std::movesentado allí, diciéndote que ha ocurrido un movimiento.

El problema es que no lo ha hecho. No se garantiza que haya sido movido de. Es posible que se haya movido, pero solo lo sabrá mirando el código fuente. No se puede distinguir solo de la firma de la función.

Recomendaciones

  • (A) Por valor: si quiere decir que una función reclama la propiedad de a unique_ptr, tómela por valor.
  • (C) Const l-value reference: si quiere decir que una función simplemente usa unique_ptrdurante la ejecución de esa función, tómela por const&. Alternativamente, pasar una &o const&para el tipo real señalado, en vez 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), tómela &&. Pero le recomiendo encarecidamente que no haga 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 std::movefunción de biblioteca estándar.

Si toma un unique_ptrvalor, puede moverse libremente. Pero el movimiento en realidad no ocurre 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 valor r no temporales no son realmente valores r. Está aquí solo con fines de demostración).

El temporaryes solo una referencia de valor r oldPtr. Es en el constructor de newPtrdonde ocurre el movimiento. unique_ptrEl constructor de movimientos (un constructor que toma un &&sí mismo) es lo que hace el movimiento real.

Si tiene un unique_ptrvalor y desea almacenarlo en algún lugar, debe usarlo std::movepara hacer el almacenamiento.


55
@Nicol: pero std::moveno nombra su valor de retorno. Recuerde que las referencias de valor con nombre son valores. ideone.com/VlEM3
R. Martinho Fernandes

31
Básicamente estoy de acuerdo con esta respuesta, pero tengo algunas observaciones. (1) No creo que haya un caso de uso válido para pasar la referencia a const lvalue: todo lo que el destinatario pueda hacer con eso, también puede hacerlo con referencia al puntero const (desnudo), o incluso mejor al puntero mismo [y no es de su incumbencia saber que la propiedad se mantiene a través de a unique_ptr; tal vez algunas otras personas que llaman necesitan la misma funcionalidad pero están reteniendo una shared_ptrllamada en su lugar] (2) por referencia lvalue podría ser útil si la función llamada modifica el puntero, por ejemplo, agregar o quitar nodos (propiedad de lista) de una lista vinculada.
Marc van Leeuwen

8
... (3) Aunque su argumento a favor de pasar por valor sobre pasar por referencia de valor tiene sentido, creo que el estándar en sí mismo siempre pasa unique_ptrvalores por referencia de valor (por ejemplo, al transformarlos en shared_ptr). La justificación de esto podría ser que es un poco más eficiente (no se realiza ningún cambio a punteros temporales) mientras que le otorga exactamente los mismos derechos a la persona que llama (puede pasar valores o valores envueltos std::move, pero no valores enteros).
Marc van Leeuwen

19
Solo para repetir lo que dijo Marc, y citando a Sutter : "No uses un const unique_ptr & como parámetro; usa widget * en su lugar"
Jon

17
Hemos descubierto un problema con el valor secundario : el movimiento se lleva a cabo durante la inicialización del argumento, que no está ordenado con respecto a otras evaluaciones de argumentos (excepto en una lista initializer_list, por supuesto). Mientras que aceptar una referencia rvalue ordena fuertemente que el movimiento ocurra después de la llamada a la función y, por lo tanto, después de la evaluación de otros argumentos. Por lo tanto, debe preferirse aceptar la referencia de valor siempre que se tome la propiedad.
Ben Voigt

57

Permítanme tratar de establecer los diferentes modos viables de pasar punteros a objetos cuya memoria es administrada por una instancia de la std::unique_ptrplantilla de clase; también se aplica a la std::auto_ptrplantilla de clase anterior (que creo que permite todos los usos que hace un puntero único, pero para los cuales además se aceptarán valores modificables donde se esperan valores, sin tener que invocar std::move), y en cierta medida también 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 (que no pueden compartir partes con otras instancias o ser circulares) son propiedad exclusiva de quien posea el listpuntero inicial . Si el código del cliente sabe que la lista que almacena nunca estará vacía, también puede optar por almacenar la primera nodedirectamente en lugar de a list. No nodees necesario definir ningún destructor : dado que los destructores para sus campos se invocan automáticamente, el destructor de puntero inteligente eliminará recursivamente toda la lista 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 para datos sin formato. Además, las funciones mismas ocasionalmente proporcionan (recursivamente) un ejemplo de código de cliente también. El typedef para, listpor supuesto, está sesgado hacia unique_ptr, pero la definición podría cambiarse para usar auto_ptro en su shared_ptrlugar sin mucha necesidad de cambiar a lo que se dice a continuación (en particular, con respecto a la seguridad de excepción garantizada sin la necesidad de escribir destructores).

Modos de pasar punteros inteligentes

Modo 0: pase 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 este caso, su función no necesita preocuparse de quién es el propietario del objeto señalado o de qué manera se gestiona 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 getmétodo o desde la dirección del operador &).

Por ejemplo, la función para calcular la longitud de dicha lista no debe ser un listargumento, 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 una variable list headpuede llamar a esta función como length(head.get()), mientras que un cliente que ha elegido almacenar una node nlista que no está vacía puede llamar 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 non- constsi 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 realiza esto, por supuesto, debe transferir la propiedad de la copia que crea, no le preocupa la propiedad de la lista que está copiando. Por lo tanto, podría definirse 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 compila en absoluto (el resultado de la llamada recursiva a copyen la lista de inicializador se une al argumento de referencia rvalue en el constructor de movimiento de unique_ptr<node>, aka list, al inicializar el nextcampo del generado node), y para la pregunta de por qué es seguro para excepciones (si durante el proceso de asignación recursiva se agota la memoria y se produce una llamada de newlanzamiento std::bad_alloc, en ese momento un puntero a la lista parcialmente construida se mantiene de forma anónima en un tipo temporal listcreado 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 nullptrporp, que después de todo se sabe que es nulo en ese punto: no se puede construir un puntero inteligente desde un puntero (en bruto) a constante , incluso cuando se sabe que es nulo.

Modo 1: pase 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 tenía la persona que llama (ya sea en una variable nombrada o en una temporal anónima) se copia en el valor del argumento en la entrada de la función y la persona que llama el puntero se ha convertido en nulo (en el caso de un temporal, la copia podría haberse omitido, pero en cualquier caso la persona que llama ha perdido el acceso al objeto señalado). Me gustaría llamar a esta llamada de 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 aclarar esto, las reglas del lenguaje requieren que la persona que llama ajuste el argumento enstd::movesi 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 temporal, dejando la variable nula.

Para los casos en que la función llamada toma posesión incondicionalmente (roba) el objeto señalado, este modo se utiliza con std::unique_ptro std::auto_ptres 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 solo hay muy pocas situaciones en las que el modo 3 a continuación no sea preferible (muy ligeramente) sobre el modo 1. Por esta razón, no proporcionaré ejemplos de uso de este modo. (Pero vea el reversedejemplo del modo 3 a continuación, donde se observa que el modo 1 funcionaría al menos también). 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_ptro std::auto_ptr): dado que se realiza una operación de movimiento real al pasar una variable de punteroppor la expresión std::move(p), no se puede suponer que ptiene 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 pse realice ningún movimiento antes de la llamada a la función, por lo que otros argumentos pueden acceder de forma segura a un valor p.

Cuando se usa con std::shared_ptr, este modo es interesante porque con una definición de función única permite que la persona que llama elija si desea guardar una copia compartida del puntero mientras crea una nueva copia compartida para que la función la use (esto sucede cuando un valor se proporciona el argumento; el constructor de copia para los punteros compartidos utilizados en la llamada aumenta el recuento de referencia), o simplemente para dar a la función una copia del puntero sin retener uno o tocar el recuento de referencia (esto sucede cuando se proporciona un argumento de valor, posiblemente un valor 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 lvalue) y void f(std::shared_ptr<X>&& 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 semántica (escribiendo en su std::move(x)lugar, como en el código de ejemplo). Entonces, 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 (modificable) referencia de valor

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 se puede utilizar para tomar posesión del objeto señalado, pero no es necesario. Este modo requiere proporcionar un argumento de valor variable modificable, correspondiente al hecho de que el efecto deseado de la función puede incluir dejar un valor útil en la variable del argumento. Una persona que llama con una expresión de valor que desea pasar a dicha función se vería obligada a almacenarla en una variable con nombre para poder realizar la llamada, ya que el lenguaje solo proporciona conversión implícita a una constantelvalue reference (refiriéndose a un temporal) de un rvalue. (A diferencia de la situación opuesta manejada por std::move, no es posible una conversión de Y&&a Y&, con Yel tipo de puntero inteligente; sin embargo, esta conversión podría obtenerse mediante una función de plantilla simple si realmente se desea; consulte https://stackoverflow.com/a/24868376 / 1436796 ). Para el caso en el que la función llamada tiene la intención de tomar posesión incondicionalmente del objeto, robando del argumento, la obligación de proporcionar un argumento lvalue está dando la señal incorrecta: la variable no tendrá un valor útil después de la llamada. Por lo tanto, debe preferirse 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, 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 señalado de una manera que implica propiedad . Por ejemplo, una función que antepone un nodo a un listproporciona un ejemplo de tal uso:

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

Claramente, sería indeseable forzar el uso de las personas que llaman 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 lo que sucede si la prependllamada falla por falta de memoria libre. Entonces la newllamada se lanzará std::bad_alloc; en este momento, dado que no se nodepudo asignar, es seguro que la referencia de valor pasada (modo 3) de std::move(l)todavía no se ha podido robar, ya que eso se haría para construir el nextcampo del nodeque no se pudo asignar. Por lo tanto, el puntero inteligente original laún conserva la lista original cuando se produce el error; esa lista será destruida adecuadamente por el destructor de puntero inteligente, o en caso de lque sobreviva gracias a una catchcláusula 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í. Notablemente, en la declaración final, el puntero que se encuentra (*p)->nextdentro del nodo que se va a eliminar se desvincula (por release, lo que devuelve el puntero pero hace que el original sea nulo) antes reset (implícitamente) de destruir ese nodo (cuando destruye el valor anterior mantenido 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 los internos de la implementación del operador de asignación de movimiento de la std::unique_ptrinstancia list; el estándar dice 20.7.1.2.3; 2 que este operador debe actuar "como si fuera llamando reset(u.release())", por lo que el tiempo debe ser seguro aquí también.)

Tenga en cuenta que prependy remove_firstno puede ser llamado por los clientes que almacenan un local de nodevariable para un siempre lista no vacía, y con razón, ya que las implementaciones dadas no podía trabajar para estos casos.

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

Este es el modo preferido para usar cuando simplemente toma posesión del puntero. Me gustaría llamar a este método llamar con cheque : la persona que llama debe aceptar renunciar a la propiedad, como si proporcionara 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" significa concretamente que las personas que llaman tienen que ajustar un argumento std::move(como en el modo 1) si es un valor l (si es un valor r, la parte "renunciar a 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 existe alguna incertidumbre sobre la transferencia de propiedad (en uso normal), se debe preferir el modo 2 al modo 3, de modo que el uso del modo 3 implícitamente sea una señal para quienes llaman de que están renunciando a la propiedad. Uno podría replicar que solo pasar el argumento del modo 1 realmente indica la pérdida forzada de propiedad a las personas que llaman. 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 que se llama, lo que debería eliminar cualquier duda.

Es sorprendentemente difícil encontrar un ejemplo típico de nuestro listtipo que utilice el paso de argumentos en modo 3. Mover una lista bal final de otra lista aes 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 pasar el 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;
}

Se puede llamar a esta función como l = reversed(std::move(l));para revertir la lista en sí misma, pero la lista revertida 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 ldirectamente en lugar de p, pero luego acceder a él cada vez implicaría un nivel adicional de indirección); por lo tanto, la diferencia con el paso del argumento del modo 1 es mínima. De hecho, utilizando ese modo, el argumento podría haber servido directamente como variable local, evitando así ese movimiento inicial; Esta 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 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 que transfieren la propiedad de los punteros inteligentes usando el modo 3. Un caso convincente en particular es el constructor std::shared_ptr<T>(auto_ptr<T>&& p). Ese constructor solía (in std::tr1) tomar una referencia de valor de l modificable (al igual que el auto_ptr<T>&constructor de copia) y, por lo tanto, podría llamarse con un valor de auto_ptr<T>l pcomo in std::shared_ptr<T> q(p), después de lo cual pse ha restablecido a nulo. Debido al cambio del modo 2 al 3 en el paso de argumentos, este antiguo código ahora debe reescribirse std::shared_ptr<T> q(std::move(p))y continuará funcionando. Entiendo que al comité no le gustó el modo 2 aquí, pero tenían la opción de cambiar al modo 1, definiendostd::shared_ptr<T>(auto_ptr<T> p)en cambio, podrían haberse asegurado de que el código antiguo funciona sin modificación, porque (a diferencia de los punteros únicos) los punteros automáticos pueden desreferenciarse silenciosamente a un valor (el objeto puntero se restablece a nulo en el proceso). Aparentemente, el comité prefirió tanto el modo de defensa 3 sobre el modo 1, que eligió 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 sobre el modo 3 en los casos en que asumir la propiedad de otra manera tomaría la forma de mover el puntero inteligente a una variable local como en el reversedejemplo 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 indicador temporal y rechazar el puntero anterior (manejar efectivo es algo laborioso); En algunos escenarios, el puntero se puede pasar varias veces sin cambios a otra función antes de que sea realmente robado. Dicha aprobación generalmente requerirá escritura std::move(a menos que se use el modo 2), pero tenga en cuenta que esto es solo un reparto que en realidad no hace nada (en particular, no elimina la referencia), por lo que tiene un costo cero adjunto.

  • En caso de que sea 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 señalado a otra estructura de datos (y esta excepción aún no está atrapada dentro de la función en sí) ), cuando se usa el modo 1, el objeto al que hace referencia el puntero inteligente se destruirá antes de que una catchcláusula 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 la persona que llama tiene la opción de recuperar los datos del objeto en tales casos (al detectar 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 programa, 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 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 regrese por valor (y no lo use std::move en la returndeclaración). Nadie quiere obtener una referencia a un puntero que probablemente acaba de ser rechazado.


1
+1 para el Modo 0: pasar el puntero subyacente en lugar de unique_ptr. Ligeramente fuera de tema (ya que la pregunta es sobre pasar un unique_ptr) pero es simple y evita problemas.
Machta

"el modo 1 aquí no causa una pérdida de memoria ", lo que implica que el modo 3 sí causa una pérdida de memoria, lo cual no es cierto. Independientemente de si unique_ptrse ha movido o no, aún eliminará muy bien el valor si aún lo mantiene cada vez que se destruye o se reutiliza.
rustyx

@RustyX: No puedo ver cómo interpretas esa implicación, y nunca tuve la intención de decir lo que crees que implicaba. Todo lo que quise decir es que, como en otros lugares, el uso de unique_ptrpreviene una pérdida de memoria (y, en cierto sentido, cumple su contrato), pero aquí (es decir, usando el modo 1) podría causar (en circunstancias específicas) algo que podría considerarse aún más dañino. , es decir, una pérdida de datos (destrucción del valor señalado) que podría haberse evitado utilizando el modo 3.
Marc van Leeuwen

4

Sí, tienes que hacerlo si tomas el unique_ptrvalor por en el constructor. La simplicidad es algo agradable. Como no unique_ptrse puede copiar (copia privada), lo que escribiste debería darte un error de compilación.


3

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 en que edité esto por última vez: ¿Cómo paso un argumento unique_ptr a un constructor o una función?

La idea básica ::std::movees que las personas que te están pasando unique_ptrdeberían usarlo para expresar el conocimiento de que saben unique_ptrque van a perder la propiedad.

Esto significa que debería usar una referencia de valor de r unique_ptren sus métodos, no un unique_ptrsí mismo. De todos modos, esto no funcionará porque pasar un viejo simple unique_ptrrequeriría hacer una copia, y eso está explícitamente prohibido en la interfaz unique_ptr. Curiosamente, el uso de una referencia de rvalue con nombre lo convierte nuevamente 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, ::std::moveexpresa 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 tu clase haber objptrperdido repentinamente la propiedad sin una razón aparente.


2
Las referencias de valor nominadas son valores.
R. Martinho Fernandes

¿Estás seguro de que es Base fred(::std::move(objptr));y no Base::UPtr fred(::std::move(objptr));?
codablank1

1
Para agregar a mi comentario anterior: este código no se compilará. Aún debe usarlo std::moveen la implementación tanto del constructor como del método. E incluso cuando pasa por valor, la persona que llama aún debe usar std::movepara pasar valores. La principal diferencia es que con el paso por valor esa interfaz deja en claro que se perderá la propiedad. Ver el comentario de Nicol Bolas sobre otra respuesta.
R. Martinho Fernandes

@ codablank1: Sí. Estoy demostrando cómo usar el constructor y los métodos en base que toman referencias de valor.
Omnifarioso

@ R.MartinhoFernandes: Oh, interesante. Supongo que tiene sentido. Esperaba que estuvieras equivocado, pero las pruebas reales demostraron que estabas en lo correcto. Corregido ahora.
Omnifarioso

0
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é hay evtdentro handle()?


3
No hay ganancia en el uso std::forwardaquí: siempreBase::UPtr&& es un tipo de referencia rvalue y lo pasa como un rvalue. Ya se reenvió correctamente. std::move
R. Martinho Fernandes

77
Estoy totalmente en desacuerdo. Si una función toma un unique_ptrvalor por, se le garantiza que se llamó a un constructor de movimiento sobre el nuevo valor (o simplemente que se le otorgó un valor temporal). Esto asegura que la unique_ptrvariable que tiene el usuario ahora está vacía . Si lo toma en su &&lugar, solo se vaciará si su código invoca una operación de movimiento. A su manera, es posible que la variable de la que el usuario no haya sido movido. Lo que hace que el uso del usuario sea std::movesospechoso y confuso. El uso std::movesiempre debe garantizar que se haya movido algo .
Nicol Bolas

@NicolBolas: Tienes razón. Eliminaré mi respuesta porque mientras funciona, su observación es absolutamente correcta.
Omnifarioso

0

A la respuesta más votada. Prefiero pasar por rvalue reference.

Entiendo cuál es el problema que puede causar el pasar por la referencia rvalue. Pero dividamos este problema en dos lados:

  • para la persona que llama:

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

  • para el llamado:

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

Eso es todo.

Si pasa por referencia de valor, solo invocará una instrucción de "movimiento", 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 usted. Pase lo que pase por valor o referencia de valor, ¡su código es el mismo!

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

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

Sabes cuál es la mejor opción.

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

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.