¿Qué es el modismo de copiar e intercambiar?

¿Qué es este modismo y cuándo debería usarse? ¿Qué problemas resuelve? ¿El idioma cambia cuando se usa C ++ 11?

Aunque se ha mencionado en muchos lugares, no teníamos ninguna pregunta y respuesta singular de "qué es", así que aquí está. Aquí hay una lista parcial de lugares donde se mencionó anteriormente:

preguntado el 19 de julio de 10 a las 08:07

gotw.ca/gotw/059.htm de Herb Sutter -

Genial, vinculé esta pregunta desde mi respuesta para mover la semántica. -

Es una buena idea tener una explicación completa de este idioma, es tan común que todos deberían saberlo. -

Advertencia: El idioma copiar / intercambiar se usa con mucha más frecuencia de lo que es útil. A menudo es perjudicial para el rendimiento cuando no se necesita una garantía de seguridad de excepción sólida para la asignación de copias. Y cuando se necesita una seguridad de excepción sólida para la asignación de copias, se proporciona fácilmente mediante una función genérica corta, además de un operador de asignación de copias mucho más rápido. Ver slideshare.net/ripplelabs/howard-hinnant-accu2014 diapositivas 43 - 53. Resumen: copiar / intercambiar es una herramienta útil en la caja de herramientas. Pero se ha comercializado en exceso y, posteriormente, a menudo se ha abusado de él. -

@HowardHinnant: Sí, +1 a eso. Escribí esto en un momento en el que casi todas las preguntas de C ++ eran "ayudar a mi clase se bloquea cuando una copia" y esta fue mi respuesta. Es apropiado cuando solo desea una semántica de copiar / mover que funcione o lo que sea para poder pasar a otras cosas, pero no es realmente óptimo. Siéntase libre de poner un descargo de responsabilidad en la parte superior de mi respuesta si cree que eso ayudará. -

5 Respuestas

Visión general

¿Por qué necesitamos el idioma de copiar e intercambiar?

Cualquier clase que gestiona un recurso (un envoltura, como un puntero inteligente) necesita implementar Los tres grandes. Si bien los objetivos y la implementación del constructor de copia y el destructor son sencillos, el operador de asignación de copia es posiblemente el más matizado y difícil. ¿Cómo deberia hacerse? ¿Qué escollos deben evitarse?

La modismo de copiar e intercambiar es la solución y ayuda elegantemente al operador de asignación a lograr dos cosas: evitar duplicación de códigoy proporcionando un fuerte garantía de excepción.

¿Cómo Funciona?

Conceptualmente, funciona utilizando la funcionalidad del constructor de copias para crear una copia local de los datos, luego toma los datos copiados con un swap función, intercambiando los datos antiguos con los datos nuevos. La copia temporal luego se destruye, llevándose consigo los datos antiguos. Nos queda una copia de los nuevos datos.

Para usar el lenguaje de copiar e intercambiar, necesitamos tres cosas: un constructor de copia que funcione, un destructor que funcione (ambos son la base de cualquier envoltorio, por lo que debería estar completo de todos modos) y un swap función.

Una función de intercambio es una no lanzar función que intercambia dos objetos de una clase, miembro por miembro. Podríamos tener la tentación de usar std::swap en lugar de proporcionar el nuestro, pero esto sería imposible; std::swap usa el constructor de copia y el operador de asignación de copia dentro de su implementación, ¡y finalmente estaríamos tratando de definir el operador de asignación en términos de sí mismo!

(No solo eso, sino llamadas no calificadas a swap utilizará nuestro operador de intercambio personalizado, omitiendo la construcción y destrucción innecesarias de nuestra clase que std::swap implicaría.)


Una explicación en profundidad

La meta

Consideremos un caso concreto. Queremos administrar, en una clase que de otra manera sería inútil, una matriz dinámica. Comenzamos con un constructor, un constructor de copia y un destructor que funcione:

#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 casi administra la matriz con éxito, pero necesita operator= para que funcione correctamente.

Una solución fallida

Así es como se vería una implementación ingenua:

// the hard part
dumb_array& operator=(const dumb_array& other)
{
    if (this != &other) // (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; esto ahora gestiona una matriz, sin fugas. Sin embargo, tiene tres problemas, marcados secuencialmente en el código como (n).

  1. La primera es la prueba de autoasignación.
    Esta verificación tiene dos propósitos: es una manera fácil de evitar que ejecutemos código innecesario en la autoasignación y nos protege de errores sutiles (como eliminar la matriz solo para intentar copiarla). Pero en todos los demás casos sólo sirve para ralentizar el programa y actuar como ruido en el código; la autoasignación rara vez ocurre, por lo que la mayoría de las veces esta verificación es un desperdicio.
    Sería mejor si el operador pudiera trabajar correctamente sin él.

  2. La segunda es que solo proporciona una garantía básica de excepción. Si new int[mSize] falla, *this habrá sido modificado. (Es decir, ¡el tamaño es incorrecto y los datos se han ido!)
    Para una garantía de excepción sólida, debería ser algo similar a:

     dumb_array& operator=(const dumb_array& other)
     {
         if (this != &other) // (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;
     }
    
  3. ¡El código se ha expandido! Lo que nos lleva al tercer problema: la duplicación de código.

Nuestro operador de asignación duplica efectivamente todo el código que ya hemos escrito en otro lugar, y eso es algo terrible.

En nuestro caso, el núcleo son solo dos líneas (la asignación y la copia), pero con recursos más complejos, este código puede ser bastante complicado. Debemos esforzarnos por no repetirnos nunca.

(Uno podría preguntarse: si se necesita tanto código para administrar un recurso correctamente, ¿qué pasa si mi clase administra más de uno?
Si bien esto puede parecer una preocupación válida, y de hecho requiere try/catch cláusulas, esto no es un problema.
Eso es porque una clase debe manejar un solo recurso!)

Una solucion exitosa

Como se mencionó, el idioma de copiar e intercambiar solucionará todos estos problemas. Pero ahora mismo, tenemos todos los requisitos excepto uno: un swap función. Si bien La regla de tres implica con éxito la existencia de nuestro constructor de copias, operador de asignación y destructor, en realidad debería llamarse "Los tres grandes y medio": cada vez que su clase administra un recurso, también tiene sentido proporcionar un swap función.

Necesitamos agregar la 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);
    }

    // ...
};

(Aquí es la explicación por qué public friend swap.) Ahora no solo podemos intercambiar nuestros dumb_array's, pero los swaps en general pueden ser más eficientes; simplemente intercambia punteros y tamaños, en lugar de asignar y copiar matrices completas. Aparte de esta ventaja en funcionalidad y eficiencia, ahora estamos listos para implementar el lenguaje de copiar e intercambiar.

Sin más preámbulos, nuestro operador de asignación es:

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

    return *this;
}

¡Y eso es! De un solo golpe, los tres problemas se abordan elegantemente a la vez.

¿Por qué funciona?

Primero notamos una elección importante: se toma el argumento del parámetro por valor. Si bien uno podría hacer lo siguiente con la misma facilidad (y de hecho, muchas implementaciones ingenuas del idioma lo hacen):

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

    return *this;
}

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

De cualquier manera, este método para obtener nuestro recurso es la clave para eliminar la duplicación de código: podemos usar el código del constructor de copia para hacer la copia, y nunca necesitamos repetir ningún fragmento. Ahora que la copia está hecha, estamos listos para intercambiar.

Observe que al ingresar a la función que todos los nuevos datos ya están asignados, copiados y listos para ser utilizados. Esto es lo que nos da una fuerte garantía de excepción de forma gratuita: ni siquiera ingresaremos a la función si falla la construcción de la copia y, por lo tanto, no es posible alterar el estado de *this. (Lo que hicimos manualmente antes para una fuerte garantía de excepción, el compilador lo está haciendo por nosotros ahora; qué amable).

En este punto estamos libres de casa, porque swap no lanza. Intercambiamos nuestros datos actuales con los datos copiados, alterando de forma segura nuestro estado, y los datos antiguos se colocan en temporales. Los datos antiguos se liberan cuando la función regresa. (Donde termina el alcance del parámetro y se llama a su destructor).

Debido a que el idioma no repite ningún código, no podemos introducir errores dentro del operador. Tenga en cuenta que esto significa que nos hemos librado de la necesidad de una verificación de autoasignación, lo que permite una única implementación uniforme de operator=. (Además, ya no tenemos una penalización de desempeño en las asignaciones que no son propias).

Y ese 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: la Regla de Tres es ahora La regla de cuatro (y medio). ¿Por qué? Porque no solo necesitamos poder copiar-construir nuestro recurso, también tenemos que moverlo-construirlo.

Afortunadamente para nosotros, esto es fácil:

class dumb_array
{
public:
    // ...

    // move constructor
    dumb_array(dumb_array&& other) noexcept ††
        : dumb_array() // initialize via default constructor, C++11 only
    {
        swap(*this, other);
    }

    // ...
};

¿Que está pasando aqui? Recuerde el objetivo de la construcción de movimientos: tomar los recursos de otra instancia de la clase, dejándola en un estado garantizado para ser asignable y destructible.

Entonces, lo que hemos hecho es simple: inicializar a través del constructor predeterminado (una característica de C ++ 11), luego intercambiar con other; sabemos que una instancia construida por defecto de nuestra clase se puede asignar y destruir de forma segura, por lo que sabemos other podrá hacer lo mismo, después de cambiar.

(Tenga en cuenta que algunos compiladores no admiten la delegación de constructores; en este caso, tenemos que construir manualmente la clase por defecto. Esta es una tarea desafortunada, pero afortunadamente trivial).

¿Por qué funciona eso?

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

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

Ahora si other se está inicializando con un rvalue, será construido en movimiento. Perfecto. De la misma manera que C ++ 03 nos permite reutilizar nuestra funcionalidad de constructor de copia tomando el argumento por valor, C ++ 11 automáticamente elija el constructor de movimientos cuando sea apropiado también. (Y, por supuesto, como se mencionó en el artículo vinculado anteriormente, la copia / movimiento del valor puede simplemente eliminarse por completo).

Y así concluye el idioma de copiar e intercambiar.


Notas a pie de página

* ¿Por qué establecemos mArray a nulo? Porque si se lanza algún código adicional en el operador, el destructor de dumb_array podría ser llamado; y si eso sucede sin establecerlo en nulo, ¡intentamos eliminar la memoria que ya se eliminó! Evitamos esto configurándolo en nulo, ya que eliminar nulo no es una operación.

† Hay otras afirmaciones en las que deberíamos especializarnos std::swap para nuestro tipo, proporcione un swap junto con una función libre swap, etc. Pero todo esto es innecesario: cualquier uso adecuado de swap será a través de una llamada no calificada, y nuestra función se encontrará a través de ADL. Una función servirá.

‡ La razón es simple: una vez que tenga el recurso para usted, puede intercambiarlo y / o moverlo (C ++ 11) a cualquier lugar que necesite. Y al hacer la copia en la lista de parámetros, maximiza la optimización.

†† El constructor de movimientos generalmente debe ser noexcept, de lo contrario algún código (p. ej. std::vector lógica de cambio de tamaño) utilizará el constructor de copia incluso cuando un movimiento tenga sentido. Por supuesto, solo márquelo como no, excepto si el código interno no arroja excepciones.

Respondido 24 Abr '21, 12:04

@GMan: Yo diría que una clase que administra varios recursos a la vez está condenada al fracaso (la seguridad de las excepciones se convierte en una pesadilla) y recomendaría encarecidamente que una clase administre UN recurso O que tenga funciones comerciales y administradores de uso. - Matthieu M.

No entiendo por qué el método de intercambio se declara como amigo aquí. - szx

@asd: Para permitir que se encuentre a través de ADL. - GManNickG

@neuviemeporte: con el paréntesis, los elementos de las matrices se inicializan por defecto. Sin, no están inicializados. Dado que en el constructor de copia estaremos sobrescribiendo los valores de todos modos, podemos omitir la inicialización. - GManNickG

@neuviemeporte: Necesitas tu swap que se encontrará durante la ADL si desea que funcione en la mayoría del código genérico con el que se encontrará, como boost::swap y otras instancias de intercambio. El intercambio es un tema complicado en C ++ y, en general, todos estamos de acuerdo en que un único punto de acceso es lo mejor (para mantener la coherencia), y la única forma de hacerlo en general es una función gratuita (int no puede tener un miembro de intercambio, por ejemplo). Ver mi pregunta para algunos antecedentes. - GManNickG

La asignación, en esencia, consta de dos pasos: derribando el antiguo estado del objeto y construyendo su nuevo estado como una copia del estado de algún otro objeto.

Básicamente, eso es lo que incinerador de basuras y constructor de copias hacer, por lo que la primera idea sería delegarles el trabajo. Sin embargo, dado que la destrucción no debe fallar, mientras que la construcción sí puede, en realidad queremos hacerlo al revés: primero realiza la parte constructiva y, si tiene éxito, entonces haz la parte destructiva. El lenguaje de copiar e intercambiar es una forma de hacer precisamente eso: primero llama al constructor de copias de una clase para crear un objeto temporal, luego intercambia sus datos con los temporales y luego permite que el destructor temporal destruya el estado anterior.
Como swap() se supone que nunca falla, la única parte que podría fallar es la construcción de la copia. Eso se realiza primero y, si falla, no se cambiará nada en el objeto de destino.

En su forma refinada, copiar e intercambiar se implementa haciendo que la copia se realice inicializando el parámetro (sin referencia) del operador de asignación:

T& operator=(T tmp)
{
    this->swap(tmp);
    return *this;
}

respondido 24 mar '20, 18:03

Creo que mencionar el proxeneta es tan importante como mencionar la copia, el canje y la destrucción. El intercambio no es mágicamente seguro para excepciones. Es seguro para excepciones porque intercambiar punteros es seguro para excepciones. Tu no tener para usar un pimpl, pero si no lo hace, debe asegurarse de que cada intercambio de un miembro sea seguro para excepciones. Eso puede ser una pesadilla cuando estos miembros pueden cambiar y es trivial cuando están escondidos detrás de un proxeneta. Y luego, viene el costo del proxeneta. Lo que nos lleva a la conclusión de que, a menudo, la seguridad de excepción conlleva un costo en el rendimiento. - Wilhelmtell

std::swap(this_string, that) no proporciona una garantía de no tirar. Proporciona una fuerte excepción de seguridad, pero no una garantía de no tirar. - Wilhelmtell

@wilhelmtell: En C ++ 03, no se mencionan excepciones potencialmente lanzadas por std::string::swap (que es llamado por std::swap). En C ++ 0x, std::string::swap is noexcept y no debe lanzar excepciones. - James McNellis

@sbi @JamesMcNellis está bien, pero el punto sigue en pie: si tienes miembros de tipo de clase, debes asegurarte de que intercambiarlos no es posible. Si tiene un solo miembro que es un puntero, eso es trivial. De lo contrario, no lo es. - Wilhelmtell

@wilhelmtell: Pensé que ese era el punto del intercambio: nunca lanza y siempre es O (1) (sí, lo sé, std::array...) - sbi

Ya hay algunas buenas respuestas. Me enfocaré principalmente sobre lo que creo que les falta - una explicación de los "contras" con el idioma de copiar e intercambiar ...

¿Qué es el modismo de copiar e intercambiar?

Una forma de implementar el operador de asignación en términos de una función de intercambio:

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

La idea fundamental es que:

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

  • que la adquisición se puede intentar antes de modificar el estado actual del objeto (es decir, *this) si se hace una copia del nuevo valor, razón por la cual rhs es aceptado por valor (es decir, copiado) en lugar de por referencia

  • intercambiando el estado de la copia local rhs y *this is generalmente relativamente fácil de hacer sin posibles fallas / excepciones, dado que la copia local no necesita ningún estado en particular después (solo necesita un ajuste de estado para que se ejecute el destructor, al igual que para un objeto emocionado desde en> = C ++ 11)

¿Cuándo debería usarse? (¿Qué problemas resuelve [/crear]?)

  • Cuando desee que el objeto asignado no se vea afectado por una asignación que arroja una excepción, suponiendo que tenga o pueda escribir un swap con una fuerte garantía de excepción, e idealmente una que no pueda fallar /throw.. †

  • Cuando desee una forma limpia, fácil de entender y robusta de definir el operador de asignación en términos de constructor de copia (más simple), swap y funciones destructoras.

    • La autoasignación realizada como copia e intercambio evita casos extremos que a menudo se pasan por alto. ‡

  • Cuando cualquier penalización de rendimiento o uso de recursos momentáneamente mayor creado por tener un objeto temporal adicional durante la asignación no es importante para su aplicación. ⁂

swap lanzamiento: generalmente es posible intercambiar de manera confiable los miembros de datos que los objetos rastrean por puntero, pero los miembros de datos que no son punteros que no tienen un intercambio libre de lanzamiento, o para los cuales el intercambio debe implementarse como X tmp = lhs; lhs = rhs; rhs = tmp; y la construcción de copias o la asignación pueden arrojar, aún tienen el potencial de fallar dejando algunos miembros de datos intercambiados y otros no. Este potencial se aplica incluso a C ++ 03 std::stringes como James comenta sobre otra respuesta:

@wilhelmtell: En C ++ 03, no se mencionan excepciones potencialmente lanzadas por std :: string :: swap (que es llamado por std :: swap). En C ++ 0x, std :: string :: swap es noexcept y no debe lanzar excepciones. - James McNellis 22 de diciembre de 10 a las 15:24


‡ La implementación del operador de asignación que parece sensata cuando se asigna desde un objeto distinto puede fallar fácilmente para la autoasignación. Si bien puede parecer inimaginable que el código del cliente incluso intente la autoasignación, puede suceder con relativa facilidad durante las operaciones de algoritmo en contenedores, con x = f(x); codigo donde f es (quizás solo para algunos #ifdef ramas) una macro ala #define f(x) x o una función que devuelve una referencia a x, o incluso código (probablemente ineficiente pero conciso) como 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 autoasignación, el código anterior elimina x.p_;, puntos p_ en una región de montón recién asignada, luego intenta leer el no inicializado datos allí (comportamiento indefinido), si eso no hace nada demasiado extraño, copy intenta una autoasignación a cada 'T' recién destruida!


⁂ El lenguaje de copiar e intercambiar puede introducir ineficiencias o limitaciones debido al uso de un temporal adicional (cuando el parámetro del operador es una copia construida):

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

Aquí, un escrito a mano Client::operator= podría comprobar si *this ya está conectado al mismo servidor que rhs (quizás enviando un código de "reinicio" si es útil), mientras que el enfoque de copiar e intercambiar invocaría al constructor de copia que probablemente se escribiría para abrir una conexión de socket distinta y luego cerrar la original. Eso no solo podría significar una interacción de red remota en lugar de una simple copia de variable en proceso, sino que podría incumplir los límites del cliente o servidor en los recursos o conexiones de socket. (Por supuesto, esta clase tiene una interfaz bastante horrible, pero eso es otro asunto ;-P).

Respondido 07 ago 14, 10:08

Dicho esto, una conexión de socket era solo un ejemplo: el mismo principio se aplica a cualquier inicialización potencialmente costosa, como sondeo / inicialización / calibración de hardware, generación de un grupo de subprocesos o números aleatorios, ciertas tareas de criptografía, cachés, análisis del sistema de archivos, base de datos conexiones, etc. - Tony Delroy

Hay una estafa más (masiva). Según especificaciones actuales técnicamente el objeto será no tener un operador de asignación de movimiento! Si luego se usa como miembro de una clase, la nueva clase no tendrá move-ctor generado automáticamente! Fuente: youtu.be/mYrbivnruYw?t=43m14s - user362515

El principal problema con el operador de asignación de copias de Client es que la cesión no está prohibida. - sbi

En el ejemplo del cliente, la clase debería ser no copiable. - John Z. Li

Esta respuesta es más como una adición y una ligera modificación a las respuestas anteriores.

En algunas versiones de Visual Studio (y posiblemente en otros compiladores) hay un error que es realmente molesto y no tiene sentido. Entonces, si declara / define su swap funciona así:

friend void swap(A& first, A& second) {

    std::swap(first.size, second.size);
    std::swap(first.arr, second.arr);

}

... el compilador le gritará cuando llame al swap función:

enter image description here

Esto tiene algo que ver con un friend función que se llama y this objeto que se pasa como parámetro.


Una forma de evitar esto es no usar friend palabra clave y redefinir la swap función:

void swap(A& other) {

    std::swap(size, other.size);
    std::swap(arr, other.arr);

}

Esta vez, puedes llamar swap y pasar other, haciendo feliz al compilador:

enter image description here


Después de todo, tu no necesitas utilizar una friend función para intercambiar 2 objetos. Tiene tanto sentido hacer swap una función miembro que tiene una other objeto como parámetro.

Ya tienes acceso a this objeto, por lo que pasarlo como parámetro es técnicamente redundante.

Respondido el 04 de Septiembre de 13 a las 09:09

@GManNickG dropbox.com/s/o1mitwcpxmawcot/example.cpp dropbox.com/s/jrjrn5dh1zez5vy/Untitled.jpg. Ésta es una versión simplificada. Parece ocurrir un error cada vez que un friend la función se llama con *this parámetro - Oleksiy

@GManNickG como dije, es un error y podría funcionar bien para otras personas. Solo quería ayudar a algunas personas que pudieran tener el mismo problema que yo. Probé esto con Visual Studio 2012 Express y 2013 Preview y lo único que hizo que desapareciera fue mi modificación: Oleksiy

@GManNickG no encajaría en un comentario con todas las imágenes y ejemplos de código. Y está bien si la gente vota en contra, estoy seguro de que hay alguien que está teniendo el mismo error; la información de esta publicación puede ser justo lo que necesitan. - Oleksiy

tenga en cuenta que esto es solo un error en el resaltado del código IDE (IntelliSense) ... Se compilará bien sin advertencias / errores. - Amro

Informe el error de VS aquí si aún no lo ha hecho (y si no se ha solucionado) connect.microsoft.com/VisualStudio - Matt

Me gustaría agregar una advertencia cuando se trata de contenedores compatibles con asignadores de estilo C ++ 11. El intercambio y la asignación tienen una semántica sutilmente diferente.

Para concreción, consideremos un contenedor. std::vector<T, A>, Donde A es un tipo de asignador con estado, y compararemos las siguientes funciones:

void fs(std::vector<T, A> & a, std::vector<T, A> & b)
{ 
    a.swap(b);
    b.clear(); // not important what you do with b
}

void fm(std::vector<T, A> & a, std::vector<T, A> & b)
{
    a = std::move(b);
}

El propósito de ambas funciones fs y fm es dar a el estado que b tenía inicialmente. Sin embargo, hay una pregunta oculta: ¿Qué sucede si a.get_allocator() != b.get_allocator()? La respuesta es, depende. Vamos a escribir AT = std::allocator_traits<A>.

  • If AT::propagate_on_container_move_assignment is std::true_type, entonces fm reasigna el asignador de a con el valor de b.get_allocator(), de lo contrario no lo hace, y a sigue utilizando su asignador original. En ese caso, los elementos de datos deben intercambiarse individualmente, ya que el almacenamiento de a y b no es compatible.

  • If AT::propagate_on_container_swap is std::true_type, entonces fs intercambia datos y asignadores de la forma esperada.

  • If AT::propagate_on_container_swap is std::false_type, entonces necesitamos una verificación dinámica.

    • If a.get_allocator() == b.get_allocator(), los dos contenedores utilizan un almacenamiento compatible y el intercambio se realiza de la forma habitual.
    • Sin embargo, si a.get_allocator() != b.get_allocator(), el programa tiene comportamiento indefinido (cf. [container.requirements.general / 8].

El resultado es que el intercambio se ha convertido en una operación no trivial en C ++ 11 tan pronto como su contenedor comienza a admitir asignadores con estado. Ese es un "caso de uso avanzado", pero no es del todo improbable, ya que las optimizaciones de movimiento generalmente solo se vuelven interesantes una vez que su clase administra un recurso, y la memoria es uno de los recursos más populares.

Respondido el 24 de junio de 14 a las 12:06

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.