c++ c++11 overloading assignment-operator copy-and-swap

c++ - ¿Cuándo se pasa la sobrecarga por referencia(l-value y r-value) a pass-by-value?



c++11 overloading (1)

Para los tipos cuyo operador de asignación de copias puede reciclar recursos, el intercambio con una copia casi nunca es la mejor manera de implementar el operador de asignación de copias. Por ejemplo, mira std::vector :

Esta clase administra un buffer de tamaño dinámico y mantiene tanto una capacity (la longitud máxima que puede contener el buffer), como un size (la longitud actual). Si el operador de asignación de copia vector se implementa swap , entonces no importa qué, siempre se asigna un nuevo buffer si rhs.size() != 0 .

Sin embargo, si lhs.capacity() >= rhs.size() , no es necesario asignar ningún nuevo búfer. Uno puede simplemente asignar / construir los elementos de rhs a lhs . Cuando el tipo de elemento es trivialmente copiable, esto puede reducirse a nada más que memcpy . Esto puede ser mucho, mucho más rápido que asignar y desasignar un buffer.

Mismo problema para std::string .

Mismo problema para MyType cuando MyType tiene miembros de datos que son std::vector y / o std::string .

Solo hay 2 veces que desea considerar la implementación de la asignación de copia con swap:

  1. Usted sabe que el método de swap (incluida la construcción de copia obligatoria cuando el rhs es un valor l) no será terriblemente ineficiente.

  2. Usted sabe que siempre necesitará que el operador de asignación de copias tenga la fuerte garantía de excepción de seguridad.

Si no está seguro acerca de 2, en otras palabras, cree que el operador de asignación de copias a veces puede necesitar la garantía de seguridad de excepción fuerte, no implemente la asignación en términos de intercambio. Es fácil para sus clientes obtener la misma garantía si proporciona uno de:

  1. Un intercambio sin excepción.
  2. Un operador de asignación de movimiento sin excepción.

Por ejemplo:

template <class T> T& strong_assign(T& x, T y) { using std::swap; swap(x, y); return x; }

o:

template <class T> T& strong_assign(T& x, T y) { x = std::move(y); return x; }

Ahora habrá algunos tipos en los que implementar la asignación de copia con swap tendrá sentido. Sin embargo, estos tipos serán la excepción, no la regla.

En:

void push_back(const value_type& val); void push_back(value_type&& val);

Imagine vector<big_legacy_type> donde:

class big_legacy_type { public: big_legacy_type(const big_legacy_type&); // expensive // no move members ... };

Si tuviéramos solo:

void push_back(value_type val);

Entonces push_back un big_legacy_type en un vector requeriría 2 copias en lugar de 1, incluso cuando la capacity fuera suficiente. Eso sería un desastre, en cuanto al rendimiento.

Actualizar

Aquí hay un HelloWorld que debería poder ejecutar en cualquier plataforma compatible con C ++ 11:

#include <vector> #include <random> #include <chrono> #include <iostream> class X { std::vector<int> v_; public: explicit X(unsigned s) : v_(s) {} #if SLOW_DOWN X(const X&) = default; X(X&&) = default; X& operator=(X x) { v_.swap(x.v_); return *this; } #endif }; std::mt19937_64 eng; std::uniform_int_distribution<unsigned> size(0, 1000); std::chrono::high_resolution_clock::duration test(X& x, const X& y) { auto t0 = std::chrono::high_resolution_clock::now(); x = y; auto t1 = std::chrono::high_resolution_clock::now(); return t1-t0; } int main() { const int N = 1000000; typedef std::chrono::duration<double, std::nano> nano; nano ns(0); for (int i = 0; i < N; ++i) { X x1(size(eng)); X x2(size(eng)); ns += test(x1, x2); } ns /= N; std::cout << ns.count() << "ns/n"; }

He codificado el operador de asignación de copias de X dos maneras:

  1. Implícitamente, lo que equivale a llamar al operador de asignación de copias de vector .
  2. Con la expresión copiar / intercambiar, sugestivamente bajo la macro SLOW_DOWN . Pensé en nombrarlo SLEEP_FOR_AWHILE , pero de esta manera es mucho peor que las sentencias sleep si estás en un dispositivo alimentado por batería.

La prueba construye algunos vector<int> size vector<int> tamaño aleatorio entre 0 y 1000, y los asigna un millón de veces. Hace que cada uno, sume los tiempos, y luego encuentra el tiempo promedio en nanosegundos de punto flotante y lo imprime. Si dos llamadas consecutivas a su reloj de alta resolución no devuelven algo menos de 100 nanosegundos, es posible que desee aumentar la longitud de los vectores.

Aquí están mis resultados:

$ clang++ -std=c++11 -stdlib=libc++ -O3 test.cpp $ a.out 428.348ns $ a.out 438.5ns $ a.out 431.465ns $ clang++ -std=c++11 -stdlib=libc++ -O3 -DSLOW_DOWN test.cpp $ a.out 617.045ns $ a.out 616.964ns $ a.out 618.808ns

Veo un porcentaje de ejecución del 43% para el modificador copiar / intercambiar con esta prueba simple. YMMV.

La prueba anterior, en promedio, tiene suficiente capacidad en la mitad del tiempo. Si llevamos esto a cualquiera de los extremos:

  1. lhs tiene suficiente capacidad todo el tiempo.
  2. lhs tiene suficiente capacidad, nada de tiempo.

luego, la ventaja de rendimiento de la asignación de copia predeterminada sobre el idioma de copia / intercambio varía de aproximadamente 560% a 0%. La expresión de copiar / intercambiar nunca es más rápida y puede ser mucho más lenta (para esta prueba).

¿Quieres velocidad? Medida.

Lo he visto decir que un operator= escrito para tomar un parámetro del mismo tipo por valor sirve como operador de asignación de copia y operador de asignación de movimiento en C ++ 11:

Foo& operator=(Foo f) { swap(f); return *this; }

Donde la alternativa sería más del doble de líneas con mucha repetición de código y posible error:

Foo& operator=(const Foo& f) { Foo f2(f); swap(f2); return *this; } Foo& operator=(Foo&& f) { Foo f2(std::move(f)); swap(f2); return *this; }

¿En qué circunstancias es preferible que la sobrecarga ref-to-const y r-value pase por valor, o cuando es necesario? Estoy pensando en std::vector::push_back , por ejemplo, que se define como dos sobrecargas:

void push_back (const value_type& val); void push_back (value_type&& val);

Siguiendo el primer ejemplo en el que pasar por valor sirve como operador de asignación de copia y operador de asignación de movimiento , ¿no podría definirse push_back en el estándar para ser una función única?

void push_back (value_type val);