¿Deberíamos pasar un shared_ptr por referencia o por valor?


270

Cuando una función toma un shared_ptr(de boost o C ++ 11 STL), lo está pasando:

  • por referencia constante: void foo(const shared_ptr<T>& p)

  • o por valor void foo(shared_ptr<T> p):?

Preferiría el primer método porque sospecho que sería más rápido. ¿Pero realmente vale la pena o hay problemas adicionales?

¿Podría dar los motivos de su elección o, en su caso, por qué cree que no importa?


14
El problema es que no son equivalentes. La versión de referencia grita "Voy a usar un alias shared_ptr, y puedo cambiarlo si quiero", mientras que la versión de valor dice "Voy a copiar tu shared_ptr, así que mientras pueda cambiarlo, nunca lo sabrás". ) Un parámetro de referencia constante es la solución real, que dice "Voy a alias algunos shared_ptr, y prometo no cambiarlo" (¡Lo cual es extremadamente similar a la semántica por valor!)
GManNickG

2
Hola, me interesaría la opinión de ustedes sobre la devolución de un shared_ptrmiembro de la clase. ¿Lo haces por const-refs?
Johannes Schaub - litb

La tercera posibilidad es usar std :: move () con C ++ 0x, esto intercambia ambos shared_ptr
Tomaka17

@Johannes: lo devolvería por referencia constante solo para evitar cualquier copia / recuento. Por otra parte, devuelvo todos los miembros por referencia constante a menos que sean primitivos.
GManNickG

Respuestas:


229

Scott, Andrei y Herb han discutido y respondido esta pregunta durante la sesión Ask Us Anything en C ++ y más allá de 2011 . Mire a partir de las 4:34 sobre shared_ptrrendimiento y corrección .

En breve, no hay razón para pasar por valor, a menos que el objetivo sea compartir la propiedad de un objeto (por ejemplo, entre diferentes estructuras de datos o entre diferentes hilos).

A menos que pueda moverlo, optimizarlo como lo explica Scott Meyers en el video de conversación vinculado anteriormente, pero eso está relacionado con la versión real de C ++ que puede usar.

Se produjo una actualización importante de esta discusión durante el panel interactivo de la conferencia GoingNative 2012 : ¡Pregúntenos cualquier cosa! que vale la pena ver, especialmente a partir de las 22:50 .


55
pero como se muestra aquí, es más barato pasar por valor: stackoverflow.com/a/12002668/128384 no debería tenerse en cuenta también (al menos para argumentos de constructor, etc., donde un shared_ptr se convertirá en miembro de la clase)?
stijn

2
@stijn Sí y no. Las preguntas y respuestas que señala están incompletas, a menos que aclare la versión del estándar C ++ al que se refiere. Es muy fácil difundir reglas generales de nunca / siempre que son simplemente engañosas. A menos que los lectores se tomen el tiempo para familiarizarse con el artículo y las referencias de David Abrahams, o tengan en cuenta la fecha de publicación frente al estándar C ++ actual. Entonces, ambas respuestas, la mía y la que usted señaló, son correctas dado el momento de la publicación.
mloskot

1
"a menos que haya múltiples subprocesos " no, MT no es de ninguna manera especial.
curioso

3
Llego súper tarde a la fiesta, pero mi razón para querer pasar shared_ptr por valor es que hace que el código sea más corto y bonito. Seriamente. Value*es corto y legible, pero es malo, así que ahora mi código está lleno const shared_ptr<Value>&y es significativamente menos legible y solo ... menos ordenado. Lo que solía ser void Function(Value* v1, Value* v2, Value* v3)ahora void Function(const shared_ptr<Value>& v1, const shared_ptr<Value>& v2, const shared_ptr<Value>& v3), ¿y la gente está de acuerdo con esto?
Alex

77
@Alex La práctica común es crear alias (typedefs) justo después de la clase. Para su ejemplo: class Value {...}; using ValuePtr = std::shared_ptr<Value>;entonces su función se vuelve más simple: void Function(const ValuePtr& v1, const ValuePtr& v2, const ValuePtr& v3)y obtiene el máximo rendimiento. Por eso usas C ++, ¿no? :)
4LegsDrivenCat

92

Aquí está la toma de Herb Sutter

Pauta: no pase un puntero inteligente como parámetro de función a menos que desee usar o manipular el puntero inteligente en sí, como compartir o transferir la propiedad.

Pauta: Exprese que una función almacenará y compartirá la propiedad de un objeto de montón utilizando un parámetro de valor compartido shared_ptr.

Directriz: Use un parámetro shared_ptr & non-const solo para modificar shared_ptr. Use un const shared_ptr & como parámetro solo si no está seguro de si tomará una copia y compartirá la propiedad; de lo contrario, utilice el widget * en su lugar (o, si no es anulable, un widget &).


3
Gracias por el enlace a Sutter. Es un excelente artículo. No estoy de acuerdo con él en el widget *, prefiero <widget &> opcional si C ++ 14 está disponible. widget * es demasiado ambiguo del código anterior.
Eponymous

3
+1 para incluir widget * y widget & as posibilidades. Solo para elaborar, pasar widget * o widget & es probablemente la mejor opción cuando la función no está examinando / modificando el objeto puntero en sí. La interfaz es más general, ya que no requiere un tipo de puntero específico, y el problema de rendimiento del recuento de referencia shared_ptr se esquiva.
tgnottingham

44
Creo que esta debería ser la respuesta aceptada hoy, debido a la segunda directriz. Claramente invalida la respuesta aceptada actual, que dice: no hay razón para pasar por valor.
mbrt

62

Personalmente, usaría una constreferencia. No es necesario incrementar el recuento de referencia solo para disminuirlo nuevamente en aras de una llamada de función.


1
No rechacé su respuesta, pero antes de que esto sea una cuestión de preferencia, hay ventajas y desventajas en cada una de las dos posibilidades a considerar. Y sería bueno saber y discutir estas ventajas y desventajas. Después, todos pueden tomar una decisión por sí mismos.
Danvil

@Danvil: teniendo en cuenta cómo shared_ptrfunciona, el único inconveniente posible de no pasar por referencia es una ligera pérdida de rendimiento. Hay dos causas aquí. a) la característica de alias de puntero significa que se copian datos de punteros más un contador (quizás 2 para referencias débiles), por lo que es un poco más costoso copiar la ronda de datos. b) el recuento de referencia atómica es ligeramente más lento que el antiguo código de incremento / decremento simple, pero es necesario para ser seguro para subprocesos. Más allá de eso, los dos métodos son los mismos para la mayoría de los intentos y propósitos.
Evan Teran

37

Pase por constreferencia, es más rápido. Si necesita almacenarlo, digamos en algún contenedor, la ref. El conteo se incrementará automáticamente por la operación de copia.


44
El voto negativo se debe a su opinión sin ningún número que lo respalde.
kwesolowski

22

Ejecuté el siguiente código, una vez footomando el shared_ptrby const&y otra vez footomando el shared_ptrby.

void foo(const std::shared_ptr<int>& p)
{
    static int x = 0;
    *p = ++x;
}

int main()
{
    auto p = std::make_shared<int>();
    auto start = clock();
    for (int i = 0; i < 10000000; ++i)
    {
        foo(p);
    }    
    std::cout << "Took " << clock() - start << " ms" << std::endl;
}

Usando VS2015, compilación de lanzamiento x86, en mi procesador Intel Core 2 quad (2.4GHz)

const shared_ptr&     - 10ms  
shared_ptr            - 281ms 

La versión de copia por valor fue un orden de magnitud más lenta.
Si está llamando a una función sincrónicamente desde el hilo actual, prefiera la const&versión.


1
¿Puedes decir qué compilador, plataforma y configuración de optimización usaste?
Carlton

Usé la compilación de depuración de vs2015, actualicé la respuesta para usar la compilación de lanzamiento ahora.
tcb

1
Tengo curiosidad por saber si cuando la optimización está activada, obtienes los mismos resultados con ambos
Elliot Woods

2
La optimización no ayuda mucho. El problema es la contención de bloqueo en el recuento de referencia en la copia.
Alex

1
Ese no es el punto. Tal foo()función ni siquiera debería aceptar un puntero compartido en primer lugar porque no está usando este objeto: debería aceptar ay int&hacer p = ++x;, llamando foo(*p);desde main(). Una función acepta un objeto puntero inteligente cuando necesita hacer algo con él, y la mayoría de las veces, lo que necesita hacer es moverlo ( std::move()) a otro lugar, por lo que un parámetro de valor no tiene costo.
eepp

15

Desde C ++ 11 deberías tomarlo por valor sobre const y con más frecuencia de lo que piensas.

Si está tomando std :: shared_ptr (en lugar del tipo T subyacente), lo está haciendo porque desea hacer algo con él.

Si desea copiarlo en algún lugar, tiene más sentido tomarlo por copia, y std :: moverlo internamente, en lugar de tomarlo por constante y luego copiarlo. Esto se debe a que le permite a la persona que llama la opción a su vez std :: mover el shared_ptr al llamar a su función, ahorrándose así un conjunto de operaciones de incremento y decremento. O no. Es decir, el llamador de la función puede decidir si necesita o no std :: shared_ptr después de llamar a la función, y dependiendo de si se mueve o no. Esto no se puede lograr si pasa por const &, por lo que es preferible tomarlo por valor.

Por supuesto, si la persona que llama necesita su shared_ptr durante más tiempo (por lo tanto, no puede std :: moverlo) y no desea crear una copia simple en la función (digamos que quiere un puntero débil, o solo a veces quiere copiarlo, dependiendo de alguna condición), entonces una constante y aún podría ser preferible.

Por ejemplo, deberías hacer

void enqueue(std::shared<T> t) m_internal_queue.enqueue(std::move(t));

encima

void enqueue(std::shared<T> const& t) m_internal_queue.enqueue(t);

Porque en este caso siempre creas una copia internamente


1

Sin saber el costo de tiempo de la operación de copia shared_copy donde está el incremento y decremento atómico, sufrí un problema de uso de CPU mucho mayor. Nunca esperé que el incremento y la disminución atómicos puedan tomar tanto costo.

Siguiendo el resultado de mi prueba, el incremento y decremento atómico int32 tarda 2 o 40 veces más que el incremento y decremento no atómico. Lo obtuve en 3GHz Core i7 con Windows 8.1. El primer resultado sale cuando no ocurre contención, el segundo cuando ocurre una alta posibilidad de contienda. Tengo en cuenta que las operaciones atómicas son, por fin, un bloqueo basado en hardware. Cerradura es cerradura. Malo para el rendimiento cuando se produce la contención.

Experimentando esto, siempre uso byref (const shared_ptr &) que byval (shared_ptr).


1

Hubo una publicación reciente en el blog: https://medium.com/@vgasparyan1995/pass-by-value-vs-pass-by-reference-to-const-c-f8944171e3ce

Entonces la respuesta a esto es: (casi) nunca pases de largo const shared_ptr<T>&.
Simplemente pasa la clase subyacente en su lugar.

Básicamente, los únicos tipos de parámetros razonables son:

  • shared_ptr<T> - Modificar y tomar posesión
  • shared_ptr<const T> - No modificar, tomar posesión
  • T& - Modificar, sin propiedad
  • const T& - No modificar, sin propiedad
  • T - No modificar, sin propiedad, barato para copiar

Como señaló @accel en https://stackoverflow.com/a/26197326/1930508, el consejo de Herb Sutter es:

Use un const shared_ptr & como parámetro solo si no está seguro de si tomará o no una copia y compartirá la propiedad

¿Pero en cuántos casos no estás seguro? Entonces esta es una situación rara


0

Es conocido el problema de que pasar shared_ptr por valor tiene un costo y debe evitarse si es posible.

El costo de pasar por shared_ptr

La mayoría de las veces pasaría shared_ptr por referencia, e incluso mejor por referencia constante.

La directriz central de cpp tiene una regla específica para pasar shared_ptr

R.34: tome un parámetro shared_ptr para expresar que una función es propietaria de una parte

void share(shared_ptr<widget>);            // share -- "will" retain refcount

Un ejemplo de cuándo es realmente necesario pasar shared_ptr por valor es cuando la persona que llama pasa un objeto compartido a una persona que llama asincrónica, es decir, la persona que llama queda fuera de alcance antes de que la persona que llama complete su trabajo. La persona que llama debe "extender" la vida útil del objeto compartido tomando un share_ptr por valor. En este caso, pasar una referencia a shared_ptr no funcionará.

Lo mismo ocurre para pasar un objeto compartido a un hilo de trabajo.


-4

shared_ptr no es lo suficientemente grande, ni su constructor \ destructor hace suficiente trabajo para que haya suficiente sobrecarga de la copia para preocuparse por el rendimiento de paso por referencia vs paso por copia.


15
¿Lo has medido?
curioso

2
@stonemetal: ¿Qué pasa con las instrucciones atómicas durante la creación de nuevo shared_ptr?
Quarra

Es un tipo que no es POD, por lo que en la mayoría de las ABI incluso pasarlo "por valor" en realidad pasa un puntero. El problema no es la copia real de bytes. Como puede ver en la salida de asm, pasar un shared_ptr<int>valor por toma más de 100 instrucciones x86 (incluidas las lockinstrucciones de edición costosas para aumentar / disminuir atómicamente el recuento de referencia). Pasar por referencia constante es lo mismo que pasar un puntero a cualquier cosa (y en este ejemplo en el explorador del compilador Godbolt, la optimización de llamada de cola convierte esto en un simple jmp en lugar de una llamada: godbolt.org/g/TazMBU ).
Peter Cordes

TL: DR: Esto es C ++ donde los constructores de copias pueden hacer mucho más trabajo que simplemente copiar los bytes. Esta respuesta es basura total.
Peter Cordes

2
stackoverflow.com/questions/3628081/shared-ptr-horrible-speed Como ejemplo, los punteros compartidos pasados ​​por valor vs paso por referencia ve una diferencia de tiempo de ejecución de aproximadamente el 33%. Si está trabajando en un código crítico de rendimiento, los punteros desnudos le proporcionan un mayor aumento de rendimiento. Así que asegúrese de pasar por const ref si lo recuerda, pero no es un gran problema si no lo hace. Es mucho más importante no usar shared_ptr si no lo necesita.
stonemetal
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.