¿Qué es una copia y cambio de idioma?

¿Qué es este idioma y cuándo debe usarse? ¿Qué problemas resuelve él? ¿Cambia el idioma con C ++ 11?

Aunque se mencionó en muchos lugares, no tuvimos ninguna pregunta y respuesta especial sobre "qué es esto", de modo que aquí está. Aquí hay una lista parcial de lugares donde se mencionó anteriormente:

1717
19 июля '10 в 11:42 2010-07-19 11:42 GManNickG está programado para el 19 de julio de 2010 a las 11:42 2010-07-19 11:42
@ 5 respuestas

Revisión

¿Por qué necesitamos el lenguaje de copia e intercambio?

Cualquier clase que administre un recurso (un shell, como un puntero inteligente) debe implementar The Three Three . Aunque los objetivos y la implementación del constructor de copia y el destructor son simples, el operador de asignación de copia es quizás el más complejo y con más matices. ¿Cómo hacer esto? ¿Qué escollos hay que evitar?

El lenguaje de copia e intercambio es una solución y asiste elegantemente al operador de asignación para lograr dos cosas: evitar la duplicación de código y proporcionar una garantía confiable de excepción .

¿Cómo funciona?

Conceptualmente , funciona utilizando la funcionalidad de copia-constructor para crear una copia local de los datos, luego toma los datos copiados utilizando la función de swap , reemplazando los datos antiguos con datos nuevos. Entonces la copia temporal se destruye, llevándose los datos antiguos con ella. Os dejamos una copia de los nuevos datos.

Para utilizar los modismos de copia e intercambio, necesitamos tres cosas: el constructor de la instancia de trabajo, el destructor de trabajo (ambos son la base de cualquier shell, por lo que deben completarse de todos modos) y la función de swap .

La función de intercambio es una función no metálica que intercambia dos objetos de clase, un miembro por un miembro. Podemos sentirnos tentados a utilizar std::swap lugar de proporcionar el nuestro, pero eso sería imposible; std::swap utiliza una instancia de constructor y un operador de asignación de copia en su implementación, y eventualmente trataremos de definir un operador de asignación en términos de nosotros mismos.

(No solo esto, sino también las llamadas de swap no calificadas utilizarán nuestro operador de intercambio personalizado, omitiendo construcciones innecesarias y destruyendo nuestra clase, lo que implicaría std::swap ).


Explicación detallada

Propósito de

Considere un caso específico. Queremos controlar la clase de lo contrario inútil con una matriz dinámica. Comencemos con el constructor de trabajo, el constructor de copia y el destructor:

 #include <algorithm> // std::copy #include <cstddef> // std::size_t class dumb_array { public: // (default) constructor dumb_array(std::size_t size = 0) : mSize(size), mArray(mSize ? new int[mSize]() : nullptr) { } // copy-constructor dumb_array(const dumb_array other) : mSize(other.mSize), mArray(mSize ? new int[mSize] : nullptr), { // note that this is non-throwing, because of the data // types being used; more attention to detail with regards // to exceptions must be given in a more general case, however std::copy(other.mArray, other.mArray + mSize, mArray); } // destructor ~dumb_array() { delete [] mArray; } private: std::size_t mSize; int* mArray; }; 

Esta clase administra casi con éxito la matriz, pero para una operación correcta requiere el operator= .

Mala decisión

Aquí es cómo se vería una implementación ingenua:

 // the hard part dumb_array operator=(const dumb_array other) { if (this !=  // (1) { // get rid of the old data... delete [] mArray; // (2) mArray = nullptr; // (2) *(see footnote for rationale) // ...and put in the new mSize = other.mSize; // (3) mArray = mSize ? new int[mSize] : nullptr; // (3) std::copy(other.mArray, other.mArray + mSize, mArray); // (3) } return *this; } 

Y decimos que hemos terminado; ahora controla la matriz sin fugas. Sin embargo, tiene tres problemas etiquetados secuencialmente en el código como (n) .

  • La primera es la prueba de homing. Esta verificación tiene dos propósitos: es una manera fácil de evitar que ejecutemos códigos innecesarios para la autoasignación y nos protege de errores sutiles (por ejemplo, eliminar una matriz solo para copiar y copiar). Pero en todos los demás casos, simplemente ralentiza el programa y actúa como ruido en el código; el autoaprendizaje rara vez ocurre, por lo que la mayoría de las veces este control es un desperdicio Sería mejor si el operador pudiera trabajar normalmente sin él.

  • En segundo lugar, proporciona sólo la garantía de excepción básica. Si new int[mSize] no funciona, *this cambiará. (Es decir, el tamaño es incorrecto, y los datos se han ido). Para una garantía de excepción confiable, esto debería ser algo como:

     dumb_array operator=(const dumb_array other) { if (this !=  // (1) { // get the new data ready before we replace the old std::size_t newSize = other.mSize; int* newArray = newSize ? new int[newSize]() : nullptr; // (3) std::copy(other.mArray, other.mArray + newSize, newArray); // (3) // replace the old data (all are non-throwing) delete [] mArray; mSize = newSize; mArray = newArray; } return *this; } 
  • ¡El código se ha expandido! Esto nos lleva al tercer problema: la duplicación de código. Nuestro operador de destino duplica efectivamente todo el código que hemos escrito en otro lugar, y esto es algo terrible.

En nuestro caso, su núcleo consta de solo dos líneas (selección y copia), pero con recursos más complejos, este código hinchado puede ser bastante complejo. Debemos esforzarnos por nunca repetirnos.

(Podría pensar: si este código es necesario para administrar correctamente un recurso, ¿qué sucede si mi clase administra más de uno? Aunque puede parecer un problema real, y de hecho requiere un try / catch no trivial, esto no es un problema. Esto se debe a que debe administrar un solo recurso !)

Decisión exitosa

Como ya se mencionó, el lenguaje de copia e intercambio solucionará todos estos problemas. Pero ahora mismo tenemos todos los requisitos excepto uno: un swap . Si bien la regla de tres implica la existencia de nuestro constructor de copia, operador de asignación y destructor, realmente debería llamarse el "Big Three and Half": en cualquier momento que su clase controle un recurso, también tiene sentido proporcionar un swap .

Necesitamos agregar funcionalidad de intercambio a nuestra clase, y lo hacemos de la siguiente manera:

 class dumb_array { public: // ... friend void swap(dumb_array first, dumb_array second) // nothrow { // enable ADL (not necessary in our case, but good practice) using std::swap; // by swapping the members of two objects, // the two objects are effectively swapped swap(first.mSize, second.mSize); swap(first.mArray, second.mArray); } // ... }; 

( Esto explica por qué el public friend swap cambia). Ahora no solo podemos intercambiar nuestro dumb_array , sino que los intercambios generalmente pueden ser más eficientes; simplemente cambia los punteros y los tamaños, en lugar de asignar y copiar matrices completas. Además de esta ventaja en funcionalidad y eficiencia, ahora estamos listos para implementar el lenguaje de copia e intercambio.

Sin más preámbulos, nuestra declaración de asignación es:

 dumb_array operator=(dumb_array other) // (1) { swap(*this, other); // (2) return *this; } 

Y esto Con un solo golpe, los tres problemas se resuelven elegantemente de inmediato.

¿Por qué funciona esto?

Primero, notamos una elección importante: el argumento del parámetro se toma por valor. Aunque puede hacer lo siguiente con la misma facilidad (y, de hecho, muchas implementaciones ingenuas de modismos):

 dumb_array operator=(const dumb_array other) { dumb_array temp(other); swap(*this, temp); return *this; } 

Perdemos una importante oportunidad de optimización . No solo eso, sino que esta elección es crucial en C ++ 11, como se explicará más adelante. (En general, la guía es muy útil: si va a hacer algo en una función, deje que el compilador lo haga en la lista de parámetros. ‡)

En cualquier caso, este método para obtener nuestro recurso es la clave para eliminar la duplicación de código: utilizamos el código del constructor de copias para crear una copia y nunca tenemos que repetirla. Ahora que la copia está hecha, estamos listos para el intercambio.

Tenga en cuenta que cuando ingresa a una función, todos los datos nuevos ya están seleccionados, copiados y listos para usar. Esto es lo que nos da una fuerte garantía de exclusión gratuita: ni siquiera ingresaremos a la función si falla la construcción de la copia, y por lo tanto es imposible cambiar el estado de *this . (Lo que hicimos a mano antes, para una garantía confiable de una excepción, el compilador lo hace por nosotros ahora, como amabilidad).

En este momento estamos libres de casa, porque el swap no se da por vencido. Intercambiamos nuestros datos actuales con datos copiados, cambiando de forma segura nuestro estado, y los datos antiguos caen en datos temporales. Luego, los datos antiguos se emiten cuando la función regresa. (Cuando se llama al final del área de parámetros y su destructor).

Como el idioma no repite ningún código, no podemos introducir errores en el operador. Tenga en cuenta que esto significa que eliminamos la necesidad de una verificación de autodeterminación que permita una implementación uniforme uniforme del operator= . (Además, ya no tenemos una penalización de rendimiento por asignaciones incorrectas).

Y este es el idioma de copiar e intercambiar.

¿Qué pasa con C ++ 11?

La próxima versión de C ++, C ++ 11, hace un cambio muy importante en la forma en que administramos los recursos: ahora la regla tres es ahora la Regla de cuatro (y media). Por que Ya que no solo necesitamos copiar-construir nuestro recurso, también necesitamos moverlo-construirlo .

Afortunadamente para nosotros es fácil:

 class dumb_array { public: // ... // move constructor dumb_array(dumb_array other) : dumb_array() // initialize via default constructor, C++11 only { swap(*this, other); } // ... }; 

¿Qué está pasando aquí? Recuerde el objetivo de la construcción de movimientos: tomar recursos de otra instancia de una clase, dejándola en un estado garantizado como asignable y destructible.

Entonces, lo que hemos hecho es simple: inicializar con la ayuda del constructor predeterminado (función C ++ 11), luego reemplazar other ; Sabemos que una instancia predeterminada de nuestra clase puede ser asignada y destruida de manera segura, por lo que sabemos que la other puede hacer lo mismo después de ser reemplazada.

(Tenga en cuenta que algunos compiladores no admiten la delegación de constructores, en cuyo caso debemos crear manualmente una clase predeterminada. Esta es una tarea desafortunada, pero afortunadamente, trivial).

¿Por qué funciona esto?

Este es el único cambio que debemos hacer a nuestra clase, entonces, ¿por qué funciona? Recuerde la decisión importante que tomamos para hacer que un parámetro sea un valor, no una referencia:

 dumb_array operator=(dumb_array other); // (1) 

Ahora, si el other inicializa con el valor r, se construirá en la dirección de viaje. Genial De manera similar, C ++ 03 nos permite reutilizar nuestra funcionalidad para el constructor de copia, tomando el argumento por valor, C ++ 11 seleccionará automáticamente el constructor de movimiento cuando sea necesario. (Y, por supuesto, como se mencionó en el artículo vinculado anteriormente, copiar / mover un valor puede eliminarse por completo).

Y así termina el idioma de copiar e intercambiar.


Notas al pie

¿Por qué ponemos mArray en nulo? Porque, si se lanza algún código adicional en la declaración, se puede llamar al dumb_array del destructor; y si esto sucede sin establecer su valor a cero, ¡intentaremos eliminar la memoria que ya se ha eliminado! Evitamos esto estableciéndolo en cero, ya que eliminar nulo no es una operación.

† Hay otras declaraciones en las que debemos especializar std::swap para nuestro tipo, proporcionar el swap funciones gratuitas, etc., dentro del swap clases. Pero todo esto no es necesario: cualquier uso adecuado del swap pasará por una llamada no calificada, y nuestra función se encontrará a través de la ADL . Una función hará.

‡ El motivo es simple: si tiene un recurso para usted, puede cambiarlo y / o moverlo (C ++ 11) a cualquier lugar que deba. Y al hacer una copia en la lista de parámetros, maximiza la optimización.

1882
19 июля '10 в 11:43 2010-07-19 11:43 la respuesta la da GManNickG el 19 de julio de 2010 a las 11:43 2010-07-19 11:43

La asignación en el corazón consta de dos pasos: interrumpir el estado anterior del objeto y crear su nuevo estado como una copia de otro estado del objeto.

Básicamente, lo que hacen destructor y constructor. por lo tanto, la primera idea sería delegar el trabajo a ellos. Sin embargo, dado que la destrucción no debe fallar, mientras que la construcción puede hacerlo, realmente queremos hacerlo al revés: primero realice la parte constructiva y, si tiene éxito , realice la parte destructiva . El lenguaje de copia e intercambio es una forma de hacer eso: primero, llama al constructor de la instancia de clase para crear uno temporal, luego intercambia sus datos con el temporal y luego permite que el destructor temporal destruya el estado antiguo.
Como swap() nunca debe fallar, la única parte que puede fallar es la copia. Esto se hace primero, y si falla, no se cambiará nada en el objeto de destino.

En su forma refinada, la copia y el intercambio se implementan realizando una copia inicializando (sin referencia) el parámetro del operador de asignación:

 T operator=(T tmp) { this->swap(tmp); return *this; } 
234
19 июля '10 в 11:55 2010-07-19 11:55 la respuesta se da sbi 19 de julio, '10 a las 11:55 2010-07-19 11:55

Ya tengo buenas respuestas. Me centraré principalmente en el hecho de que, en mi opinión, no son suficientes: una explicación de los "inconvenientes" con el idioma "copia e intercambio" ...

¿Qué es copiar y reemplazar idioma?

La forma de implementar un operador de asignación en términos de la función de intercambio:

 X operator=(X rhs) { swap(rhs); return *this; } 

La idea básica es que:

  • La parte más propensa a errores de la asignación a un objeto es proporcionar los recursos que necesita el nuevo estado (por ejemplo, memoria, controladores)

  • de modo que puede intentar intentar antes de cambiar el estado actual del objeto (es decir, *this ), si se ha realizado una copia del nuevo valor, por lo tanto, rhs acepta rhs por valor (es decir, se copia) que por referencia

  • reemplazar el estado de la copia local de rhs y *this ser relativamente fácil de hacer sin fallas / excepciones potenciales, ya que la copia local no necesita ningún estado en particular (solo requiere un estado adecuado para que se ejecute el destructor, como un objeto movido desde> = C ++ 11)

¿Cuándo se debe utilizar? (¿Qué problemas resuelve [/ crear] ?)

  • Si desea que el objeto designado no se vea afectado por la asignación que genera la excepción, suponiendo que tiene o puede escribir un swap con una garantía confiable de excepción y, idealmente, uno que no puede fallar / throw . †

  • Si necesita una forma limpia, clara y confiable para definir un operador de asignación en términos del constructor de copia, el swap y las funciones destructivas (más simples).

    • La autoasignación, realizada como "copiar e intercambiar", le permite evitar casos comunes.

  • Si cualquier limitación de rendimiento o uso de recursos a corto plazo, creado con un objeto temporal adicional en el momento de la asignación, no es importante para su aplicación. ⁂

swap : como regla general, es posible reemplazar de manera confiable los elementos de datos, que los objetos rastrean por puntero, pero no elementos de datos indicativos que no tienen swap-swap o para los cuales X tmp = lhs; lhs = rhs; rhs = tmp; debe ser intercambiado X tmp = lhs; lhs = rhs; rhs = tmp; X tmp = lhs; lhs = rhs; rhs = tmp; y la copia-construcción o asignación puede ser lanzada, aún existe la posibilidad de fallar si algunos miembros de datos se intercambian y otros no. Este potencial se aplica incluso a C ++ 03 std::string , como James comenta en otra respuesta:

@wilhelmtell: No se mencionan excepciones en C ++ 03, que pueden seleccionarse utilizando std :: string :: swap (que se llama std :: swap). En C ++ 0x, std :: string :: swap noexcept y no debe generar excepciones. - James McNellis 22 de diciembre de 2010 a las 3:24 pm


‡ la implementación de un operador de asignación, que parece razonable cuando se asigna desde un objeto individual, puede fallar fácilmente para la autodeterminación. Aunque parezca inconcebible que el código del cliente incluso intente realizar la autodeterminación, puede suceder con relativa facilidad durante algunas operaciones en contenedores con código x = f(x); donde f (quizás solo para algunas ramas #ifdef ) ala macro #define f(x) x o una función que devuelve una referencia a x o incluso código (probablemente inefectivo pero breve), por ejemplo x = c1 ? x * 2 : c2 ? x / 2 : x; x = c1 ? x * 2 : c2 ? x / 2 : x; ). Por ejemplo:

 struct X { T* p_; size_t size_; X operator=(const X rhs) { delete[] p_; // OUCH! p_ = new T[size_ = rhs.size_]; std::copy(p_, rhs.p_, rhs.p_ + rhs.size_); } ... }; 

En la autodeterminación, el código anterior elimina x.p_; , p_ apunta al área recién asignada del montón, luego intenta leer los datos sin inicializar (comportamiento indefinido), si esto no hace nada extraño, la copy intenta realizar un nombre propio para cada "T" recién destruida.


⁂ El idioma "copiar e intercambiar" puede llevar a ineficiencias o restricciones debido al uso de tiempo adicional (cuando el operador-operador se construye a partir del contenido):

 struct Client { IP_Address ip_address_; int socket_; X(const X rhs) : ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_)) { } }; 

Aquí, el Client::operator= manuscrito Client::operator= puede verificar que *this ya *this conectado al mismo servidor que rhs (quizás sea un código de "reinicio" si es útil), mientras que el método de copiar y cambiar se referirá a la instancia del constructor, que probablemente se escribirá para abrir una conexión de socket separada, y luego cerrar la original. Esto puede significar no solo la interacción de red remota, sino también la simple copia de una variable de proceso en el proceso de trabajo, puede ser contraria a las restricciones del cliente o servidor en los recursos o conexiones de socket. (Por supuesto, esta clase tiene una interfaz bastante horrible, pero eso es otra cuestión; -P).

33
06 марта '14 в 17:51 2014-03-06 17:51 Respuesta dada por Tony Delroy el 6 de marzo de 2014 a las 17:51 2014-03-06 17:51

Esta respuesta es más como agregar y modificar ligeramente las respuestas anteriores.

Algunas versiones de Visual Studio (y posiblemente otros compiladores) tienen un error que es realmente molesto y sin sentido. Por lo tanto, si declara / define su función de swap siguiente manera:

 friend void swap(A first, A second) { std::swap(first.size, second.size); std::swap(first.arr, second.arr); } 

... el compilador te gritará cuando llames a la función de swap :

2019

20
ответ дан Oleksiy 04 сент. '13 в 7:50 2013-09-04 07:50