¿Cuál es el idioma de copiar y cambiar?


2003

¿Qué es este idioma y cuándo debe 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 sobre "qué es", así que aquí está. Aquí hay una lista parcial de lugares donde se mencionó anteriormente:


77

2
Impresionante, vinculé esta pregunta de mi respuesta para mover la semántica .
fredoverflow

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

16
Advertencia: El idioma de copia / intercambio se usa con mucha más frecuencia de lo que es útil. A menudo es perjudicial para el rendimiento cuando no se necesita una fuerte garantía de seguridad de excepción de la asignación de copias. Y cuando se necesita una fuerte excepción de seguridad 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.
Howard Hinnant

2
@HowardHinnant: Sí, +1 a eso. Escribí esto en un momento en que casi todas las preguntas de C ++ eran "ayudar a que mi clase se bloquee cuando una copia" y esta fue mi respuesta. Es apropiado cuando solo quieres trabajar con la semántica de copiar / mover o lo que sea para que puedas 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á.
GManNickG

Respuestas:


2184

Visión general

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

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

El modismo de copiar e intercambiar es la solución, y ayuda elegantemente al operador de asignación a lograr dos cosas: evitar la duplicación de código y proporcionar una garantía de excepción fuerte .

¿Como funciona?

Conceptualmente , funciona utilizando la funcionalidad del constructor de copias para crear una copia local de los datos, luego toma los datos copiados con una swapfunción, intercambiando los datos antiguos con los nuevos. La copia temporal se destruye y se lleva los datos antiguos. Nos queda una copia de los nuevos datos.

Para usar el idioma de copiar y cambiar, necesitamos tres cosas: un constructor de copia de trabajo, un destructor de trabajo (ambos son la base de cualquier contenedor, por lo que debe estar completo de todos modos) y una swapfunción.

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

(No solo eso, sino que las llamadas no calificadas swaputilizarán nuestro operador de intercambio personalizado, omitiendo la construcción innecesaria y la destrucción de nuestra clase que std::swapconllevaría).


Una explicación en profundidad.

La meta

Consideremos un caso concreto. Queremos gestionar, en una clase inútil, una matriz dinámica. Comenzamos con un constructor de trabajo, constructor de copia y 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 casi gestiona la matriz con éxito, pero debe operator=funcionar correctamente.

Una solución fallida

Así es como podría verse 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. El primero es el examen de autoasignación. Esta verificación tiene dos propósitos: es una manera fácil de evitar que ejecutemos códigos innecesarios 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, simplemente 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 funcionar correctamente sin él.

  2. El segundo es que solo ofrece una garantía de excepción básica. Si new int[mSize]falla, *thishabrá sido modificado. (¡Es decir, el tamaño es incorrecto y los datos se han ido!) Para una garantía de excepción fuerte, tendría que 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 de esto es solo dos líneas (la asignación y la copia), pero con recursos más complejos, este aumento de código puede ser una molestia. Debemos esforzarnos por nunca repetirnos.

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

Una solución exitosa

Como se mencionó, el idioma de copiar y cambiar solucionará todos estos problemas. Pero en este momento, tenemos todos los requisitos excepto uno: una swapfunción. Si bien The Rule of Three implica con éxito la existencia de nuestro constructor de copias, operador de asignación y destructor, en realidad debería llamarse "The Big Three and A Half": cada vez que su clase administra un recurso, también tiene sentido proporcionar una swapfunción .

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

    // ...
};

( Aquí está la explicación de por qué public friend swap). Ahora no solo podemos intercambiar los nuestros dumb_array, sino que los intercambios en general pueden ser más eficientes; simplemente intercambia punteros y tamaños, en lugar de asignar y copiar matrices enteras. Además de este bono en funcionalidad y eficiencia, ahora estamos listos para implementar el idioma de copiar y cambiar.

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! Con un solo golpe, los tres problemas se abordan con elegancia a la vez.

Por que funciona

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

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 crítica en C ++ 11, que se analiza más adelante. (En una nota general, una guía notablemente ú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 copias para hacer la copia, y nunca es necesario repetir nada. Ahora que la copia está hecha, estamos listos para intercambiar.

Observe que al ingresar a la función todos los datos nuevos ya están asignados, copiados y listos para ser utilizados. Esto es lo que nos da una garantía de excepción fuerte de forma gratuita: ni siquiera entraremos en 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 está haciendo por nosotros ahora; qué amable).

En este punto estamos sin hogar, porque swapes no tirar. Intercambiamos nuestros datos actuales con los datos copiados, alterando de forma segura nuestro estado, y los datos antiguos se colocan en el temporal. Los datos antiguos se liberan cuando vuelve la función. (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 libramos de la necesidad de una verificación de autoasignación, lo que permite una implementación uniforme única de operator=. (Además, ya no tenemos una penalización de rendimiento por no autoasignaciones).

Y ese es el idioma de copiar y cambiar.

¿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 necesitamos 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? Recordemos 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 puede asignarse y destruirse de manera segura, por lo que sabemos otherque podremos hacer lo mismo, después del intercambio.

(Tenga en cuenta que algunos compiladores no son compatibles con 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 necesitamos hacer en nuestra clase, entonces, ¿por qué funciona? Recuerde la decisión cada vez más importante que tomamos para hacer que el parámetro sea un valor y no una referencia:

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

Ahora, si otherse está inicializando con un valor r, se construirá en movimiento . Perfecto. Del mismo modo, C ++ 03 nos permite reutilizar nuestra funcionalidad de constructor de copia tomando el argumento por valor, C ++ 11 también elegirá automáticamente el constructor de movimiento cuando sea apropiado. (Y, por supuesto, como se mencionó en el artículo vinculado anteriormente, la copia / movimiento del valor simplemente se puede eludir por completo).

Y así concluye el modismo de copiar y cambiar.


Notas al pie

* ¿Por qué nos ponemos mArraya nulo? Porque si se arroja algún código adicional en el operador, se dumb_arraypodría llamar al destructor de ; y si eso sucede sin configurarlo como nulo, intentamos eliminar la memoria que ya se ha eliminado. Evitamos esto estableciéndolo en nulo, ya que eliminar nulo no es una operación.

† Hay otras afirmaciones de que debemos especializarnos std::swappara nuestro tipo, proporcionar una swapfunción libre junto a su clase swap, etc. Pero todo esto es innecesario: cualquier uso adecuado swapserá a través de una llamada no calificada, y nuestra función será encontrado 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 donde sea necesario. Y al hacer la copia en la lista de parámetros, maximiza la optimización.

†† El constructor de movimientos debería ser noexcept, de lo contrario, algún código (por ejemplo, la std::vectorlógica de cambio de tamaño) usará el constructor de copias incluso cuando un movimiento tenga sentido. Por supuesto, solo márquelo sin excepción, si el código interno no arroja excepciones.


17
@GMan: Yo diría que una clase que administra varios recursos a la vez está condenada al fracaso (la seguridad de excepción se convierte en una pesadilla) y recomendaría encarecidamente que una clase administre UN recurso O tenga funcionalidad comercial y use gerentes.
Matthieu M.

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

99
@asd: para permitir que se encuentre a través de ADL.
GManNickG

8
@neuviemeporte: con el paréntesis, los elementos de la matriz se inicializan por defecto. Sin ellos no están inicializados. Como en el constructor de copia sobrescribiremos los valores de todos modos, podemos omitir la inicialización.
GManNickG

10
@neuviemeporte: necesita swapque lo encuentren durante ADL si desea que funcione en la mayoría de los códigos genéricos que encontrará, como boost::swapy otras instancias de intercambio. El intercambio es un problema complicado en C ++ y, en general, todos hemos llegado a un acuerdo en que un único punto de acceso es el mejor (por coherencia), y la única forma de hacerlo en general es una función libre ( intno puede tener un miembro de intercambio, por ejemplo). Vea mi pregunta para algunos antecedentes.
GManNickG

274

La asignación, en esencia, son dos pasos: derribar el estado anterior del objeto y construir su nuevo estado como una copia del estado de algún otro objeto.

Básicamente, eso es lo que hacen el destructor y el constructor de la copia , 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 podría hacerlo , en realidad queremos hacerlo al revés : primero realicemos la parte constructiva y, si eso tiene éxito, luego hagamos la parte destructiva . El modismo de copiar y cambiar es una forma de hacer exactamente eso: primero llama al constructor de copias de una clase para crear un objeto temporal, luego intercambia sus datos con los temporales y luego deja que el destructor del temporal destruya el estado anterior.
Ya queswap()se supone que nunca falla, la única parte que puede 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, la copia y el intercambio se implementa al realizar la copia inicializando el parámetro (sin referencia) del operador de asignación:

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

1
Creo que mencionar el pimpl es tan importante como mencionar la copia, el intercambio y la destrucción. El intercambio no es mágicamente seguro para las excepciones. Es seguro para excepciones porque el intercambio de punteros es seguro para excepciones. No tiene que 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 ocultos detrás de un grano. Y luego, luego viene el costo del grano. Lo que nos lleva a la conclusión de que a menudo la seguridad de excepción conlleva un costo en el rendimiento.
wilhelmtell

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

11
@wilhelmtell: en C ++ 03, no se mencionan las excepciones potencialmente lanzadas por std::string::swap(que se llama por std::swap). En C ++ 0x, std::string::swapes noexcepty no debe arrojar excepciones.
James McNellis

2
@sbi @JamesMcNell está bien, pero el punto sigue en pie: si tiene miembros de clase, debe asegurarse de intercambiarlos. Si tiene un solo miembro que es un puntero, entonces es trivial. De lo contrario no lo es.
wilhelmtell

2
@wilhelmtell: pensé que ese era el punto de intercambio: nunca arroja y siempre es O (1) (sí, lo sé, std::array...)
sbi

44

Ya hay algunas buenas respuestas. Me centraré principalmente en lo que creo que les falta: una explicación de los "contras" con el modismo de copiar e intercambiar ...

¿Cuál es el idioma de copiar y cambiar?

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 necesita el nuevo estado (por ejemplo, memoria, descriptores)

  • esa adquisición se puede intentar antes de modificar el estado actual del objeto (es decir, *thissi se realiza una copia del nuevo valor, por lo que rhsse acepta por valor (es decir, copiado) en lugar de por referencia

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

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

  • Cuando desee que el asignado se oponga sin que se vea afectado por una tarea que arroje una excepción, suponiendo que tenga o pueda escribir una swapcon una fuerte garantía de excepción, e idealmente una que no pueda fallar / throw.. †

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

    • La autoasignación realizada como copia e intercambio evita los casos límite que se pasan por alto con frecuencia. ‡

  • 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. ⁂

swaplanzamiento: generalmente es posible intercambiar de manera confiable miembros de datos que los objetos rastrean por puntero, pero miembros de datos sin puntero que no tienen un intercambio de lanzamiento libre, o para el cual el intercambio debe implementarse como X tmp = lhs; lhs = rhs; rhs = tmp;copia de construcción o asignación puede arrojar, aún tiene el potencial de fallar dejando algunos miembros de datos intercambiados y otros no. Este potencial se aplica incluso a C ++ 03 std::stringcomo James comenta en otra respuesta:

@wilhelmtell: En C ++ 03, no se mencionan las excepciones potencialmente lanzadas por std :: string :: swap (que es llamado por std :: swap). En C ++ 0x, std :: string :: swap no es excepto y no debe lanzar excepciones. - James McNellis 22 de diciembre de 10 a 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 algo en contenedores, con un x = f(x);código donde fes (tal vez solo para algunas #ifdeframas) una macro ala #define f(x) xo una función que devuelve una referencia x, o incluso (probablemente ineficiente pero conciso) código 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, la eliminación de código anterior x.p_;, apunta p_a una región de montón recién asignada, luego intenta leer los datos no inicializados (Comportamiento indefinido), si eso no hace nada demasiado extraño, copyintenta una autoasignación a cada destruido 'T'!


Idi El idioma de copiar y cambiar puede introducir ineficiencias o limitaciones debido al uso de un temporal adicional (cuando el parámetro del operador se construye con copia):

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

Aquí, un mensaje escrito a mano Client::operator=podría verificar si *thisya está conectado al mismo servidor que rhs(tal vez 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 cierre la original. Eso no solo podría significar una interacción de red remota en lugar de una simple copia de variables en proceso, sino que podría estar en conflicto con los límites del cliente o del servidor en los recursos o conexiones del socket. (Por supuesto, esta clase tiene una interfaz bastante horrible, pero ese es otro asunto ;-P).


44
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, escaneos del sistema de archivos, base de datos conexiones, etc.
Tony Delroy

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

3
El principal problema con el operador de asignación de copia Clientes que la asignación no está prohibida.
sbi

En el ejemplo del cliente, la clase debe hacerse no copiable.
John Z. Li

25

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 otros compiladores) hay un error que es realmente molesto y no tiene sentido. Entonces, si declara / define su swapfunción de esta manera:

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 a la swapfunción:

ingrese la descripción de la imagen aquí

Esto tiene algo que ver con una friendfunción que se llama y un thisobjeto que se pasa como parámetro.


Una forma de evitar esto es no usar friendpalabras clave y redefinir la swapfunción:

void swap(A& other) {

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

}

Esta vez, solo puede llamar swapy pasar other, haciendo feliz al compilador:

ingrese la descripción de la imagen aquí


Después de todo, no necesita usar una friendfunción para intercambiar 2 objetos. Tiene tanto sentido hacer swapuna función miembro que tiene un otherobjeto como parámetro.

Ya tiene acceso al thisobjeto, por lo que pasarlo como parámetro es técnicamente redundante.


1
@GManNickG dropbox.com/s/o1mitwcpxmawcot/example.cpp dropbox.com/s/jrjrn5dh1zez5vy/Untitled.jpg . Esta es una versión simplificada. Parece que ocurre un error cada vez friendque se llama a una función con el *thisparámetro
Oleksiy

1
@GManNickG como dije, es un error y podría funcionar bien para otras personas. Solo quería ayudar a algunas personas que podrían 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

8
@GManNickG no cabe en un comentario con todas las imágenes y ejemplos de código. Y está bien si las personas votan negativamente, estoy seguro de que hay alguien por ahí que está recibiendo el mismo error; la información en esta publicación podría ser justo lo que necesitan.
Oleksiy

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

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

15

Me gustaría agregar una palabra de advertencia cuando se trata de contenedores con asignación 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 Ahay algún 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 fsy fmes dar ael estado que btenía inicialmente. Sin embargo, hay una pregunta oculta: ¿qué sucede si a.get_allocator() != b.get_allocator()? La respuesta es, depende. Escritura de Let AT = std::allocator_traits<A>.

  • Si AT::propagate_on_container_move_assignmentes así std::true_type, fmreasigna el asignador de acon el valor de b.get_allocator(), de lo contrario no lo hace, y acontinúa utilizando su asignador original. En ese caso, los elementos de datos deben intercambiarse individualmente, ya que el almacenamiento de ay bno es compatible.

  • Si AT::propagate_on_container_swapes así std::true_type, fsintercambia datos y asignadores de la manera esperada.

  • Si AT::propagate_on_container_swapes así std::false_type, entonces necesitamos una verificación dinámica.

    • Si a.get_allocator() == b.get_allocator(), entonces los dos contenedores usan almacenamiento compatible, y el intercambio se realiza de la manera habitual.
    • Sin embargo, si a.get_allocator() != b.get_allocator()el programa tiene un 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 algo 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.

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.