¿Por qué debería std::move un std::shared_ptr?
Estuve revisando el código fuente de Clang y encontré este fragmento:
void CompilerInstance::setInvocation(
std::shared_ptr<CompilerInvocation> Value) {
Invocation = std::move(Value);
}
¿Por qué querría std::move
un std::shared_ptr
?
¿Tiene algún sentido transferir la propiedad de un recurso compartido?
¿Por qué no haría esto en su lugar?
void CompilerInstance::setInvocation(
std::shared_ptr<CompilerInvocation> Value) {
Invocation = Value;
}
Creo que lo único que las otras respuestas no enfatizaron lo suficiente es el punto de velocidad .
std::shared_ptr
El recuento de referencia es atómico . aumentar o disminuir el recuento de referencia requiere un incremento o decremento atómico . Esto es cien veces más lento que el incremento/decremento no atómico , sin mencionar que si incrementamos y disminuimos el mismo contador terminamos con el número exacto, desperdiciando una tonelada de tiempo y recursos en el proceso.
Al mover el shared_ptr
en lugar de copiarlo, "robamos" el recuento de referencias atómicas y anulamos el otro shared_ptr
. "robar" el recuento de referencias no es atómico y es cien veces más rápido que copiarlo shared_ptr
(y provocar un incremento o disminución de la referencia atómica ).
Tenga en cuenta que esta técnica se utiliza únicamente para optimización. copiarlo (como sugirió) es igual de bueno en cuanto a funcionalidad.
Al usarlo move
, evita aumentar y luego disminuir inmediatamente el número de acciones. Eso podría ahorrarle algunas operaciones atómicas costosas en términos de uso.
Las operaciones de movimiento (como el constructor de movimientos) std::shared_ptr
son económicas , ya que básicamente son "robo de punteros" (del origen al destino; para ser más precisos, todo el bloque de control de estado se "roba" del origen al destino, incluida la información del recuento de referencias). .
En su lugar, las operaciones de copia al std::shared_ptr
invocar un aumento del recuento de referencias atómicas (es decir, no solo ++RefCount
en un RefCount
miembro de datos entero, sino, por ejemplo, InterlockedIncrement
en Windows), lo cual es más costoso que simplemente robar punteros/estado.
Entonces, analizando en detalle la dinámica del recuento de árbitros de este caso:
// shared_ptr<CompilerInvocation> sp;
compilerInstance.setInvocation(sp);
Si pasa sp
por valor y luego toma una copia dentro del CompilerInstance::setInvocation
método, tiene:
- Al ingresar al método, el
shared_ptr
parámetro se copia y se construye: ref count incremento atómico . - Dentro del cuerpo del método, copia el
shared_ptr
parámetro en el miembro de datos: ref count atomic increment . - Al salir del método, el
shared_ptr
parámetro se destruye: ref count atomic decrement .
Tiene dos incrementos atómicos y un decremento atómico, para un total de tres operaciones atómicas .
En cambio, si pasa el shared_ptr
parámetro por valor y luego std::move
dentro del método (como se hace correctamente en el código de Clang), tiene:
- Al ingresar al método, el
shared_ptr
parámetro se copia y se construye: ref count incremento atómico . - Dentro del cuerpo del método, ingresa
std::move
elshared_ptr
parámetro en el miembro de datos: ¡el recuento de referencias no cambia ! Solo estás robando punteros/estado: no están involucradas operaciones costosas de recuento de ref atómico. - Al salir del método, el
shared_ptr
parámetro se destruye; pero desde que te mudaste en el paso 2, no hay nada que destruir, ya que elshared_ptr
parámetro ya no apunta a nada. Nuevamente, en este caso no ocurre ningún decremento atómico.
En pocas palabras: en este caso obtienes solo un incremento atómico de recuento de referencias, es decir, solo una operación atómica.
Como puede ver, esto es mucho mejor que dos incrementos atómicos más un decremento atómico (para un total de tres operaciones atómicas) para el caso de copia.
Hay dos razones para usar std::move en esta situación. La mayoría de las respuestas abordaron la cuestión de la velocidad, pero ignoraron la importante cuestión de mostrar la intención del código con mayor claridad.
Para un std::shared_ptr, std::move denota inequívocamente una transferencia de propiedad de la punta, mientras que una simple operación de copia agrega un propietario adicional. Por supuesto, si el propietario original posteriormente renuncia a su propiedad (por ejemplo, permitiendo que se destruya su std::shared_ptr), entonces se ha logrado una transferencia de propiedad.
Cuando transfieres la propiedad con std::move, es obvio lo que está sucediendo. Si utiliza una copia normal, no es obvio que la operación prevista sea una transferencia hasta que verifique que el propietario original renuncia inmediatamente a la propiedad. Como beneficio adicional, es posible una implementación más eficiente, ya que una transferencia atómica de propiedad puede evitar el estado temporal en el que el número de propietarios ha aumentado en uno (y los consiguientes cambios en el recuento de referencias).