2012-06-08 19 views
13

considerar:¿Debo std :: mover un shared_ptr en un constructor de movimiento?

#include <cstdlib> 
#include <memory> 
#include <string> 
#include <vector> 
#include <algorithm> 
#include <iterator> 
using namespace std; 

class Gizmo 
{ 
public: 
    Gizmo() : foo_(shared_ptr<string>(new string("bar"))) {}; 
    Gizmo(Gizmo&& rhs); // Implemented Below 

private: 
    shared_ptr<string> foo_; 
}; 

/* 
// doesn't use std::move 
Gizmo::Gizmo(Gizmo&& rhs) 
: foo_(rhs.foo_) 
{ 
} 
*/ 


// Does use std::move 
Gizmo::Gizmo(Gizmo&& rhs) 
: foo_(std::move(rhs.foo_)) 
{ 
} 

int main() 
{ 
    typedef vector<Gizmo> Gizmos; 
    Gizmos gizmos; 
    generate_n(back_inserter(gizmos), 10000, []() -> Gizmo 
    { 
     Gizmo ret; 
     return ret; 
    }); 

    random_shuffle(gizmos.begin(), gizmos.end()); 

} 

En el código anterior, hay dos versiones de Gizmo::Gizmo(Gizmo&&) - uno utiliza a std::move realidad movimiento la shared_ptr, y el otro sólo copia el shared_ptr.

Ambas versiones parecen funcionar en la superficie. Una diferencia (la única diferencia que puedo ver) es en la versión no move, el recuento de referencia del shared_ptr se incrementa temporalmente, pero solo brevemente.

Normalmente seguiría adelante y move el shared_ptr, pero solo para ser claro y consistente en mi código. ¿Me estoy perdiendo una consideración aquí? ¿Debo preferir una versión sobre la otra para cualquier razón técnica?

+2

Mover en un constructor de movimientos es al menos semánticamente consistente ... – ildjarn

+1

¿Por qué mantienes la cadena en un shared_ptr? Un shared_ptr como miembro-variable es a menudo un signo de mal diseño. –

+1

Mover en un constructor de movimientos está en línea con lo que el compilador generaría automáticamente. –

Respuesta

16

El problema principal aquí no es la pequeña diferencia de rendimiento debido al incremento atómico extra y la disminución en shared_ptr, sino que la semántica de la operación es inconsistente a menos que realice un movimiento.

Aunque se supone que el recuento de referencia del shared_ptr solo será temporal, no existe dicha garantía en el idioma. El objeto fuente desde el que se está moviendo puede ser temporal, pero también podría tener una vida útil mucho más larga. Podría ser una variable llamada que ha sido fundido a una rvalue referencia (digamos std::move(var)), en cuyo caso, al no movimiento del shared_ptr todavía se está manteniendo compartían la propiedad con la fuente del movimiento, y si el destino shared_ptr tiene un alcance menor, entonces la vida útil del objeto puntiagudo se extenderá innecesariamente.

+0

Me pregunto al usar movimientos, ¿hasta qué punto debemos pensar para nosotros mismos, "este movimiento podría degradarse a una copia"? Obviamente lo hacemos cuando escribimos el código de la plantilla para un 'MoveConstructible' arbitrario tipo' T', ya que no necesita tener un constructor de movimiento en absoluto, mucho menos uno que modifique la fuente. También obviamente, es pobre QoI si un objeto movido contiene recursos innecesariamente, por lo que si 'Gizmo' tiene un constructor de movimiento entonces debería ser uno bueno. Pero creo que es una cuestión de convención y estilo de codificación cómo cruz tenemos derecho a ser cuando no lo es. –

+0

+1, esto es a lo que me refería en mi comentario sobre la respuesta de James. Así poner. – ildjarn

+0

Para otro ejemplo, una implementación válida de una asignación de movimiento es llamar a 'swap'. El estado del objeto movido desde no está especificado y, en particular, se permite que sea el estado del objeto movido. Pero estaría un poco molesto porque los recursos del objeto * moved-to * permanecen indefinidamente. –

11

Se prefiere el uso de move: debe ser más eficiente que una copia porque no requiere el incremento atómico adicional ni la disminución del recuento de referencias.

+0

También existen diferencias semánticas: ¿se esperaría que un '' Gizmo' movido '' mantuviera vivo su estado interno compartido? Personalmente, me parecería sorprendente, y esperaría que un objeto movido desde fuera liberara todo el estado compartido. – ildjarn

+1

@ildjarn: estoy de acuerdo, aunque no afectará la corrección del programa de ninguna manera: el objeto movido seguirá siendo destructible y asignable a. –

13

He votado a favor la respuesta de James McNellis. Me gustaría hacer un comentario sobre su respuesta, pero mi comentario no encajará en el formato de comentario. Así que lo estoy poniendo aquí.

Una forma divertida de medir el impacto en el rendimiento de mover un shared_ptr frente a copiar uno es usar algo como vector<shared_ptr<T>> para mover o copiar un montón de ellos y medir el tiempo. La mayoría de los compiladores tienen una manera de activar/desactivar la semántica de movimiento especificando el modo de idioma (por ejemplo, -std = C++ 03 o -std = C++ 11).

Aquí es código Acabo de prueba en -O3:

#include <chrono> 
#include <memory> 
#include <vector> 
#include <iostream> 

int main() 
{ 
    std::vector<std::shared_ptr<int> > v(10000, std::shared_ptr<int>(new int(3))); 
    typedef std::chrono::high_resolution_clock Clock; 
    typedef Clock::time_point time_point; 
    typedef std::chrono::duration<double, std::micro> us; 
    time_point t0 = Clock::now(); 
    v.erase(v.begin()); 
    time_point t1 = Clock::now(); 
    std::cout << us(t1-t0).count() << "\u00B5s\n"; 
} 

Usando sonido metálico/libC++ y en -std = C++ 03 Esto muestra para mí:

195.368µs 

Cambio a - std = C++ 11 Obtengo:

16.422µs 

Su kilometraje puede variar.

+2

+1: Solo una observación. Cuando lo adapté para usar mi clase 'Gizmo' de arriba, los tiempos de las versiones' move' y '' non''ve' eran casi idénticos. Esto fue en MSVC10, usando 'boost :: chrono' en lugar de' std :: chrono', compilado x64 Release. –

+0

@JohnDibling: Interesante. Si descubres por qué hay tal disparidad en nuestros resultados, me encantaría saber sobre eso. Una cosa para intentar: poner un noexcept en tu constructor de movimientos. No sé si MSVC10 implementa esto o no. Me sorprendería si lo hiciera teniendo en cuenta qué tan tarde llegó el día. Y realmente no esperaría que esto marque la diferencia para el miembro vector :: erase. Pero, sin embargo, es lo primero que probaría. Estoy ejecutando un Intel Core i5 de 2,8 GHz (compilado para 64 bits). ¿Sus resultados fueron del orden de unos cientos de microsegundos, o algunas decenas de microsegundos? –

Cuestiones relacionadas