2011-02-02 16 views
14

No pude dormir anoche y comencé a pensar en std::swap. Aquí es el familiar versión C++ 98:Haciendo el cambio más rápido, más fácil de usar y a prueba de excepciones

template <typename T> 
void swap(T& a, T& b) 
{ 
    T c(a); 
    a = b; 
    b = c; 
} 

Si una clase Foo definida por el usuario utiliza ressources externos, esto es ineficiente. La expresión común es proporcionar un método void Foo::swap(Foo& other) y una especialización de std::swap<Foo>. Tenga en cuenta que esto no funciona con las plantillas de clase ya que no puede especializar parcialmente una plantilla de función, y la sobrecarga de nombres en el espacio de nombres std es ilegal. La solución es escribir una función de plantilla en el propio espacio de nombres y confiar en la búsqueda dependiente de los argumentos para encontrarla. Esto depende críticamente del cliente para seguir el "using std::swap idioma" en lugar de llamar directamente al std::swap. Muy frágil

En C++ 0x, si Foo tiene un constructor movimiento definido por el usuario y un operador de asignación movimiento, proporcionando una swap método personalizado y una especialización std::swap<Foo> tiene poco o ningún beneficio de rendimiento, porque la versión C++ 0x de std::swap utiliza movimientos eficientes en lugar de copias:

#include <utility> 

template <typename T> 
void swap(T& a, T& b) 
{ 
    T c(std::move(a)); 
    a = std::move(b); 
    b = std::move(c); 
} 

No tener que jugar con más swap ya tiene una gran cantidad de carga lejos del programador. Los compiladores actuales no generan constructores de movimiento y mueven operadores de asignación automáticamente, pero, hasta donde yo sé, esto cambiará. El único problema que queda entonces es la seguridad de excepciones, porque, en general, las operaciones de movimiento pueden arrojar, y esto abre una lata de gusanos. La pregunta "¿Cuál es exactamente el estado de un objeto movido desde?" complica las cosas más.

Entonces yo estaba pensando, ¿cuál es exactamente la semántica de std::swap en C++ 0x si todo va bien? ¿Cuál es el estado de los objetos antes y después del intercambio? Normalmente, el intercambio mediante operaciones de movimiento no toca los recursos externos, solo las representaciones de objeto "planas".

¿Por qué no escribir simplemente una plantilla swap que hace exactamente eso: intercambiar las representaciones del objeto?

#include <cstring> 

template <typename T> 
void swap(T& a, T& b) 
{ 
    unsigned char c[sizeof(T)]; 

    memcpy(c, &a, sizeof(T)); 
    memcpy(&a, &b, sizeof(T)); 
    memcpy(&b, c, sizeof(T)); 
} 

Esto es lo más eficiente posible: simplemente explota en la memoria sin procesar. No requiere ninguna intervención del usuario: no se deben definir métodos especiales de intercambio u operaciones de movimiento. Esto significa que incluso funciona en C++ 98 (que no tiene referencias rvalue, fíjate). Pero lo que es más importante, ahora podemos olvidarnos de los problemas de excepción de seguridad, porque memcpy nunca se lanza.

puedo ver dos problemas potenciales con este enfoque:

En primer lugar, no todos los objetos están destinados a ser intercambiado. Si un diseñador de clase oculta el constructor de copia o el operador de asignación de copia, al intentar intercambiar objetos de la clase fallará en tiempo de compilación. Simplemente podemos introducir algún código muerto que comprueba si la copia y la asignación son legales del tipo:

template <typename T> 
void swap(T& a, T& b) 
{ 
    if (false) // dead code, never executed 
    { 
     T c(a); // copy-constructible? 
     a = b; // assignable? 
    } 

    unsigned char c[sizeof(T)]; 

    std::memcpy(c, &a, sizeof(T)); 
    std::memcpy(&a, &b, sizeof(T)); 
    std::memcpy(&b, c, sizeof(T)); 
} 

Cualquier compilador decente trivialmente puede deshacerse del código muerto. (Probablemente haya mejores formas de verificar la "conformidad de intercambio", pero ese no es el punto. Lo que importa es que es posible).

En segundo lugar, algunos tipos pueden realizar acciones "inusuales" en el constructor de copias y en el operador de asignación de copias. Por ejemplo, pueden notificar a los observadores de su cambio. Considero que esto es un problema menor, porque tales tipos de objetos probablemente no deberían haber proporcionado operaciones de copia en primer lugar.

Háganme saber lo que piensa de este enfoque para el intercambio. ¿Funcionaría en la práctica? ¿Lo usarías? ¿Puedes identificar los tipos de bibliotecas donde se rompería esto? ¿Ves problemas adicionales? ¡Discutir!

+0

La mayoría de los casos de uso actuales para 'std :: swap' tendrán mejores soluciones usando la semántica de movimiento de todos modos. – aschepler

+0

Sí mover semántica y mover constructores. Vea esto, http://stackoverflow.com/questions/4820643/understanding-stdswap-what-is-the-purpose-of-tr1-remove-reference –

+2

considere 'swap (* polymorphicPtr1, * polymorphicPtr2)' ... su La función swap intercambiaría el vtable de ambos objetos también ... lo que causará estragos si alguien llama a una función virtual o la llamada para intercambiar. – smerlin

Respuesta

20

¿Por qué no simplemente escribir una plantilla swap que hace exactamente eso: de intercambio las representaciones de objeto *?

Hay muchas maneras en las que un objeto, una vez que se construya, se pueden romper al copiar los bytes que reside en. De hecho, se podría llegar a un número aparentemente interminable de casos cuando esto no haría lo correcto - aunque en la práctica podría funcionar en el 98% de todos los casos.

Eso es porque el problema subyacente a todo esto es que, aparte de en C, C++ que que no tratamos a los objetos como si fueran mera bytes prima. Es por eso que tenemos construcción y destrucción, después de todo: convertir el almacenamiento sin procesar en objetos y objetos en almacenamiento sin procesar. Una vez que se ha ejecutado un constructor, la memoria donde reside el objeto es más que solo almacenamiento en bruto. Si lo trata como si no lo fuera, romperá algunos tipos.

Sin embargo, en esencia, los objetos en movimiento no debe realizar que mucho peor que su idea, porque, una vez que comience a inline de forma recursiva las llamadas a std::move(), por lo general, en última instancia, llegar a donde muebles empotrados se mueven. (Y si hay más para moverse para algunos tipos, ¡mejor no te metas con la memoria de ellos!) De acuerdo, mover memoria en bloque suele ser más rápido que los movimientos individuales (y es poco probable que un compilador descubra que podría hacerlo). optimice los movimientos individuales a uno que abarque todo std::memcpy()), pero ese es el precio que pagamos por la abstracción que nos ofrecen los objetos opacos.Y es bastante pequeño, especialmente cuando lo comparas con la copia que solíamos hacer.

Usted puede, sin embargo, tienen un optimizado swap() usando std::memcpy() para tipos de agregados.

+0

s/praxis/practice/;-) Además, sería bueno agregar un enlace a las preguntas frecuentes de POD/aggregate. – fredoverflow

+1

@Fred, mi diccionario dice que "praxis" es una palabra en inglés perfectamente válida. ¿No es así? El inglés no es mi lengua materna, pero tengo curiosidad. –

+0

@Sergey: Google tiene 27.700.000 resultados de búsqueda para "en la práctica", pero solo 214.000 para "en la práctica" ... – fredoverflow

7

Algunos tipos se pueden intercambiar pero no se pueden copiar. Punteros inteligentes únicos son probablemente el mejor ejemplo. La verificación de la capacidad de copiado y la asignación es incorrecta.

Si T no es un tipo de POD, usar memcpy para copiar/mover es un comportamiento indefinido.


El lenguaje común es proporcionar un método void Foo :: swap (Foo & otro) y una especialización de std :: intercambio < Foo>. Tenga en cuenta que esto no funciona con plantillas de clase, ...

Una mejor expresión idiomática es un intercambio no miembro y que requiere que los usuarios llamen al intercambio no calificado, por lo que se aplica ADL. Esto también funciona con plantillas:

struct NonTemplate {}; 
void swap(NonTemplate&, NonTemplate&); 

template<class T> 
struct Template { 
    friend void swap(Template &a, Template &b) { 
    using std::swap; 
#define S(N) swap(a.N, b.N); 
    S(each) 
    S(data) 
    S(member) 
#undef S 
    } 
}; 

La clave es la declaración using para std :: swap como alternativa. La amistad para el intercambio de Template es agradable para simplificar la definición; el intercambio de NonTemplate también podría ser un amigo, pero eso es un detalle de implementación.

20

Esto romperá las instancias de clase que tienen punteros a sus propios miembros. Por ejemplo:

class SomeClassWithBuffer { 
    private: 
    enum { 
     BUFSIZE = 4096, 
    }; 
    char buffer[BUFSIZE]; 
    char *currentPos; // meant to point to the current position in the buffer 
    public: 
    SomeClassWithBuffer(); 
    SomeClassWithBuffer(const SomeClassWithBuffer &that); 
}; 

SomeClassWithBuffer::SomeClassWithBuffer(): 
    currentPos(buffer) 
{ 
} 

SomeClassWithBuffer::SomeClassWithBuffer(const SomeClassWithBuffer &that) 
{ 
    memcpy(buffer, that.buffer, BUFSIZE); 
    currentPos = buffer + (that.currentPos - that.buffer); 
} 

Ahora, si acaba de hacer memcpy(), ¿dónde apuntaría currentPos? Para la ubicación anterior, obviamente. Esto conducirá a errores muy divertidos donde cada instancia realmente usa el buffer de otro.

+1

Honestamente, hacer que un objeto 'Reader' sea copiable, construible y asignable me parece un error de diseño. – fredoverflow

+1

@Fred, es solo un ejemplo abstracto. Probablemente debería haberlo llamado "SomeClassWithBuffer", pero eso es irrelevante. –

+0

@ Matthieu, ese tema fue mencionado por el PO, así que no lo mencioné. Probablemente haya más problemas que parece al principio, también. –

1

su versión swap causará estragos si alguien la usa con tipos polimórficos.

consideran:

Base *b_ptr = new Base(); // Base and Derived contain definitions 
Base *d_ptr = new Derived(); // of a virtual function called vfunc() 
yourmemcpyswap(*b_ptr, *d_ptr); 
b_ptr->vfunc(); //now calls Derived::vfunc, while it should call Base::vfunc 
d_ptr->vfunc(); //now calls Base::vfunc while it should call Derived::vfunc 
//... 

esto está mal, porque ahora b contiene la viable del tipo Derived, por lo Derived::vfunc se invoca en un objeto que tampoco del tipo Derived.

El normales std::swap sólo se intercambia los miembros de datos de Base, así que esto está bien con std::swap

+0

Al intercambiar solo los miembros de datos de 'Base' puede romper las invariantes del objeto' Derived'. Esa es una razón por la cual no tiene mucho sentido tener un operador de asignación para objetos polimórficos. Tenga en cuenta que Bjarne Stroustrup considera que es un accidente histórico que el operador de asignación se proporcione por defecto para todas las clases definidas por el usuario. – fredoverflow

6

considero esto un tema menor, porque tales tipos de objetos probablemente deberían no han proporcionado las operaciones de copia de El primer lugar.

Eso es, simplemente, un montón de errores. Las clases que notifican a los observadores y a las clases que no deben copiarse no tienen ninguna relación. ¿Qué tal shared_ptr? Obviamente debe ser copiable, pero obviamente también notifica a un observador: el recuento de referencia. Ahora es cierto que en este caso, el recuento de referencias es el mismo después del intercambio, pero definitivamente no es cierto para todos los tipos y es especialmente no verdadero si está involucrado el multi-threading, no es cierto en el caso de una copia normal en lugar de un intercambio, etc. Esto es especialmente incorrecto para las clases que se pueden mover o intercambiar pero no copiado.

porque, en general, las operaciones se les permite moverse a tirar

Son muy seguramente no. Es virtualmente imposible garantizar una seguridad de excepción fuerte en casi cualquier circunstancia que implique movimientos cuando la jugada podría arrojar. La definición C++ 0x de la biblioteca estándar, de memoria, establece explícitamente que cualquier tipo utilizable en cualquier contenedor estándar no debe arrojarse al moverse.

Esto es tan eficiente como se pone

Eso también es erróneo. Estás asumiendo que el movimiento de cualquier objeto es puramente sus variables miembro, pero puede que no sean todas ellas. Podría tener un caché basado en la implementación y podría decidir que dentro de mi clase, no debería mover este caché. Como detalle de la implementación, es totalmente mi derecho no mover ninguna variable miembro que considere que no sea necesario mover.Sin embargo, usted desea moverlos a todos.

Ahora, es cierto que su código de muestra debería ser válido para muchas clases. Sin embargo, es extremadamente definitivamente no válido para muchas clases que son completamente y totalmente legítimas, y más importante aún, se compilará hasta esa operación de todos modos si la operación se puede reducir a eso. Esto está rompiendo perfectamente buenas clases sin ningún beneficio.

+0

+1 para señalar: "... La definición C++ 0x de la biblioteca estándar, de memoria, establece explícitamente que cualquier tipo utilizable en cualquier contenedor estándar no debe arrojarse al moverse ...." –

+0

y yo agregaría otro +1 para "... y lo más importante, se va a compilar hasta esa operación de todos modos ..." –

Cuestiones relacionadas