Si tengo una función que necesita trabajar con a shared_ptr
, ¿no sería más eficiente pasarle una referencia (para evitar copiar el shared_ptr
objeto)? ¿Cuáles son los posibles efectos secundarios negativos? Visualizo dos casos posibles:
1) dentro de la función se hace una copia del argumento, como en
ClassA::take_copy_of_sp(boost::shared_ptr<foo> &sp)
{
...
m_sp_member=sp; //This will copy the object, incrementing refcount
...
}
2) dentro de la función, el argumento solo se usa, como en
Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here
{
...
sp->do_something();
...
}
No veo en ambos casos una buena razón para pasar el boost::shared_ptr<foo>
valor por referencia en lugar de por referencia. Pasar por valor solo aumentaría "temporalmente" el recuento de referencias debido a la copia, y luego lo disminuiría al salir del alcance de la función. ¿Estoy pasando por alto algo?
Solo para aclarar, después de leer varias respuestas: estoy perfectamente de acuerdo con las preocupaciones de optimización prematura, y siempre trato de primero perfilar y luego trabajar en los puntos de acceso. Mi pregunta era más desde un punto de vista de código puramente técnico, si sabes a qué me refiero.
fuente
Respuestas:
El objetivo de una
shared_ptr
instancia distinta es garantizar (en la medida de lo posible) que mientrasshared_ptr
esté dentro del alcance, el objeto al que apunta seguirá existiendo, porque su recuento de referencias será al menos 1.Entonces, al usar una referencia a a
shared_ptr
, deshabilita esa garantía. Entonces, en tu segundo caso:¿Cómo sabe que
sp->do_something()
no explotará debido a un puntero nulo?Todo depende de lo que haya en esas secciones '...' del código. ¿Qué pasa si llama a algo durante el primer '...' que tiene el efecto secundario (en algún lugar de otra parte del código) de borrar un
shared_ptr
a ese mismo objeto? ¿Y si resulta ser el único que queda distintoshared_ptr
a ese objeto? Adiós objeto, justo donde estás a punto de intentar usarlo.Entonces, hay dos formas de responder a esa pregunta:
Examine la fuente de todo su programa con mucho cuidado hasta que esté seguro de que el objeto no morirá durante el cuerpo de la función.
Vuelva a cambiar el parámetro para que sea un objeto distinto en lugar de una referencia.
Un consejo general que se aplica aquí: no se moleste en realizar cambios arriesgados en su código por el bien del rendimiento hasta que haya cronometrado su producto en una situación realista en un generador de perfiles y haya medido de manera concluyente que el cambio que desea realizar hará un Diferencia significativa en el rendimiento.
Actualización para el comentarista JQ
Aquí hay un ejemplo artificial. Es deliberadamente simple, por lo que el error será obvio. En ejemplos reales, el error no es tan obvio porque está oculto en capas de detalles reales.
Tenemos una función que enviará un mensaje a alguna parte. Puede ser un mensaje grande, así que en lugar de usar un mensaje
std::string
que probablemente se copie a medida que se transmite a varios lugares, usamosshared_ptr
una cadena de caracteres:(Simplemente lo "enviamos" a la consola para este ejemplo).
Ahora queremos agregar una función para recordar el mensaje anterior. Queremos el siguiente comportamiento: debe existir una variable que contenga el mensaje enviado más recientemente, pero mientras se está enviando un mensaje, no debe haber ningún mensaje anterior (la variable debe restablecerse antes de enviar). Entonces declaramos la nueva variable:
Luego modificamos nuestra función de acuerdo con las reglas que especificamos:
Entonces, antes de comenzar a enviar, descartamos el mensaje anterior actual, y luego, una vez que se completa el envío, podemos almacenar el nuevo mensaje anterior. Todo bien. Aquí hay un código de prueba:
Y como era de esperar, esto se imprime
Hi!
dos veces.Ahora llega el Sr. Maintainer, que mira el código y piensa: Oye, ese parámetro
send_message
es unshared_ptr
:Obviamente, eso se puede cambiar a:
¡Piense en la mejora del rendimiento que esto traerá! (No importa que estemos a punto de enviar un mensaje generalmente grande a través de algún canal, por lo que la mejora del rendimiento será tan pequeña que no podrá medirse).
Pero el problema real es que ahora el código de prueba exhibirá un comportamiento indefinido (en las compilaciones de depuración de Visual C ++ 2010, se bloquea).
El Sr. Maintainer está sorprendido por esto, pero agrega un control defensivo
send_message
en un intento de evitar que ocurra el problema:Pero, por supuesto, sigue adelante y se bloquea, porque
msg
nunca es nulo cuandosend_message
se llama.Como digo, con todo el código tan cerca en un ejemplo trivial, es fácil encontrar el error. Pero en programas reales, con relaciones más complejas entre objetos mutables que tienen punteros entre sí, es fácil cometer el error y difícil construir los casos de prueba necesarios para detectar el error.
La solución fácil, en la que desea que una función pueda depender de que
shared_ptr
continúe siendo no nula en todo momento, es que la función asigne su propio verdaderoshared_ptr
, en lugar de depender de una referencia a un existenteshared_ptr
.La desventaja es que la copia de a
shared_ptr
no es gratuita: incluso las implementaciones "sin bloqueo" tienen que usar una operación entrelazada para respetar las garantías de subprocesamiento. Por lo tanto, puede haber situaciones en las que un programa pueda acelerarse significativamente cambiando ashared_ptr
en ashared_ptr &
. Pero este no es un cambio que se pueda realizar de forma segura en todos los programas. Cambia el significado lógico del programa.Tenga en cuenta que se produciría un error similar si usáramos en
std::string
todo en lugar destd::shared_ptr<std::string>
y en lugar de:para aclarar el mensaje, dijimos:
Entonces el síntoma sería el envío accidental de un mensaje vacío, en lugar de un comportamiento indefinido. El costo de una copia adicional de una cadena muy grande puede ser mucho más significativo que el costo de copiar una
shared_ptr
, por lo que la compensación puede ser diferente.fuente
Me encontré en desacuerdo con la respuesta más votada, así que fui a buscar opiniones de expertos y aquí están. De http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2011-Scott-Andrei-and-Herb-Ask-Us-Anything
Herb Sutter: "cuando pasa shared_ptrs, las copias son caras"
Scott Meyers: "Shared_ptr no tiene nada de especial cuando se trata de pasarlo por valor o por referencia. Use exactamente el mismo análisis que usa para cualquier otro tipo definido por el usuario. La gente parece tener esta percepción de que shared_ptr resuelve de alguna manera todos los problemas de gestión, y como es pequeño, es necesariamente económico pasarlo por valor. Tiene que ser copiado, y hay un costo asociado con eso ... es caro pasarlo por valor, así que si puedo salirme con la mía con la semántica adecuada en mi programa, lo pasaré por referencia a const o reference en su lugar "
Herb Sutter: "siempre páselos por referencia a const, y muy ocasionalmente tal vez porque sabe que lo que llamó podría modificar la cosa de la que obtuvo una referencia, tal vez entonces podría pasar por valor ... si los copia como parámetros, oh Dios mío, casi nunca necesitas superar ese recuento de referencias porque de todos modos se mantiene vivo, y deberías pasarlo por referencia, así que haz eso "
Actualización: Herb se ha expandido en esto aquí: http://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/ , aunque la moraleja de la historia es que no debería pasar shared_ptrs en absoluto "a menos que desee utilizar o manipular el puntero inteligente en sí, como para compartir o transferir la propiedad".
fuente
shared_ptr
s, no hubo acuerdo sobre el problema de traspaso de valor frente a referencia.const &
afectará a la semántica solo es fácil en muy programas simples.No recomendaría esta práctica a menos que usted y los otros programadores con los que trabaja sepan realmente lo que están haciendo.
Primero, no tiene idea de cómo podría evolucionar la interfaz de su clase y desea evitar que otros programadores hagan cosas malas. Pasar un shared_ptr por referencia no es algo que un programador debería esperar ver, porque no es idiomático y eso facilita su uso incorrecto. Programar a la defensiva: dificulta el uso incorrecto de la interfaz. Pasar por referencia solo generará problemas más adelante.
En segundo lugar, no optimice hasta que sepa que esta clase en particular será un problema. Primero perfil, y luego si su programa realmente necesita el impulso dado al pasar por referencia, entonces tal vez. De lo contrario, no se preocupe por las cosas pequeñas (es decir, las N instrucciones adicionales que se necesitan para pasar por valor) en lugar de preocuparse por el diseño, las estructuras de datos, los algoritmos y la capacidad de mantenimiento a largo plazo.
fuente
Sí, tomar una referencia está bien. No tiene la intención de otorgar propiedad compartida al método; solo quiere trabajar con él. También puede tomar una referencia para el primer caso, ya que la copia de todos modos. Pero para el primer caso, toma posesión. Existe este truco para copiarlo solo una vez:
También debe copiar cuando lo devuelva (es decir, no devuelva una referencia). Porque su clase no sabe qué está haciendo el cliente con él (podría almacenar un puntero y luego ocurre el big bang). Si más adelante resulta que es un cuello de botella (¡primer perfil!), Aún puede devolver una referencia.
Editar : Por supuesto, como señalan otros, esto solo es cierto si conoce su código y sabe que no restablece el puntero compartido pasado de alguna manera. En caso de duda, pase por valor.
fuente
Es sensato pasar
shared_ptr
por altoconst&
. Es probable que no cause problemas (excepto en el improbable caso de que el referenciadoshared_ptr
se elimine durante la llamada a la función, como lo detalla Earwicker) y probablemente será más rápido si pasa muchos de estos. Recuerda; el valor predeterminadoboost::shared_ptr
es seguro para subprocesos, por lo que copiarlo incluye un incremento seguro para subprocesos.Intente usar en
const&
lugar de solo&
, porque es posible que los objetos temporales no se pasen por una referencia que no sea constante. (Aunque una extensión de idioma en MSVC le permite hacerlo de todos modos)fuente
En el segundo caso, hacer esto es más simple:
Puedes llamarlo como
fuente
Evitaría una referencia "simple" a menos que la función pueda modificar explícitamente el puntero.
A
const &
puede ser una microoptimización sensata al llamar a funciones pequeñas, por ejemplo, para permitir más optimizaciones, como insertar algunas condiciones. Además, el incremento / decremento, ya que es seguro para subprocesos, es un punto de sincronización. Sin embargo, no esperaría que esto hiciera una gran diferencia en la mayoría de los escenarios.Generalmente, debe usar el estilo más simple a menos que tenga motivos para no hacerlo. Luego, use el
const &
consistentemente o agregue un comentario sobre por qué si lo usa solo en algunos lugares.fuente
Yo recomendaría pasar un puntero compartido por referencia constante, una semántica de que la función que se pasa con el puntero NO posee el puntero, que es un lenguaje limpio para los desarrolladores.
El único error es que en los programas de subprocesos múltiples, el objeto al que apunta el puntero compartido se destruye en otro subproceso. Por lo tanto, es seguro decir que usar la referencia constante del puntero compartido es seguro en un programa de un solo subproceso.
Pasar un puntero compartido por una referencia no constante es a veces peligroso: la razón es que las funciones de intercambio y restablecimiento que la función puede invocar en el interior para destruir el objeto que aún se considera válido después de que la función regrese.
No se trata de una optimización prematura, supongo, se trata de evitar un desperdicio innecesario de ciclos de CPU cuando tienes claro lo que quieres hacer y tus compañeros desarrolladores han adoptado firmemente el lenguaje de codificación.
Solo mis 2 centavos :-)
fuente
Parece que todos los pros y los contras aquí en realidad se pueden generalizar a CUALQUIER tipo pasado por referencia, no solo shared_ptr. En mi opinión, debes conocer la semántica de pasar por referencia, const referencia y valor y utilizarla correctamente. Pero no hay absolutamente nada de malo en pasar shared_ptr por referencia, a menos que piense que todas las referencias son malas ...
Para volver al ejemplo:
¿Cómo sabes que
sp.do_something()
no explotará debido a un puntero colgando?La verdad es que, shared_ptr o no, const o no, esto podría suceder si tiene un defecto de diseño, como compartir directa o indirectamente la propiedad
sp
entre subprocesos, mal uso de un objeto que sídelete this
, tiene una propiedad circular u otros errores de propiedad.fuente
Una cosa que no he visto mencionada todavía es que cuando pasa punteros compartidos por referencia, pierde la conversión implícita que obtiene si desea pasar un puntero compartido de clase derivada a través de una referencia a un puntero compartido de clase base.
Por ejemplo, este código producirá un error, pero funcionará si lo cambia
test()
para que el puntero compartido no se pase por referencia.fuente
Asumiré que está familiarizado con la optimización prematura y lo está preguntando con fines académicos o porque ha aislado algún código preexistente que tiene un rendimiento inferior.
Pasar por referencia está bien
Pasar por referencia constante es mejor y generalmente se puede usar, ya que no fuerza la constancia en el objeto apuntado.
Estás no en riesgo de perder el puntero debido al uso de una referencia. Esa referencia es evidencia de que tiene una copia del puntero inteligente anteriormente en la pila y solo un subproceso posee una pila de llamadas, por lo que la copia preexistente no desaparecerá.
El uso de referencias suele ser más eficaz por las razones que menciona, pero no está garantizado . Recuerde que eliminar la referencia a un objeto también puede requerir trabajo. Su escenario ideal de uso de referencia sería si su estilo de codificación involucra muchas funciones pequeñas, donde el puntero pasaría de una función a otra antes de ser utilizado.
Siempre debe evitar almacenar su puntero inteligente como referencia. Su
Class::take_copy_of_sp(&sp)
ejemplo muestra el uso correcto para eso.fuente
Suponiendo que no nos preocupa la corrección const (o más, quiere decir permitir que las funciones puedan modificar o compartir la propiedad de los datos que se pasan), pasar un boost :: shared_ptr por valor es más seguro que pasarlo por referencia como permitimos que el boost :: shared_ptr original controle su propia vida. Considere los resultados del siguiente código ...
En el ejemplo anterior, vemos que pasar sharedA por referencia permite a FooTakesReference restablecer el puntero original, lo que reduce su recuento de uso a 0, destruyendo sus datos. FooTakesValue , sin embargo, no puede restablecer el puntero original, lo que garantiza que los datos de sharedB aún sean utilizables. Cuando inevitablemente aparece otro desarrollador e intenta aprovechar la frágil existencia de sharedA , se produce el caos. El afortunado desarrollador de sharedB , sin embargo, se va a casa temprano porque todo está bien en su mundo.
La seguridad del código, en este caso, supera con creces cualquier mejora de velocidad que crea la copia. Al mismo tiempo, boost :: shared_ptr está destinado a mejorar la seguridad del código. Será mucho más fácil pasar de una copia a una referencia, si algo requiere este tipo de optimización de nicho.
fuente
Sandy escribió: "Parece que todos los pros y los contras aquí en realidad pueden generalizarse a CUALQUIER tipo pasado por referencia, no solo shared_ptr".
Es cierto hasta cierto punto, pero el objetivo de utilizar shared_ptr es eliminar las preocupaciones sobre la vida útil de los objetos y dejar que el compilador se encargue de ello. Si va a pasar un puntero compartido por referencia y permitirá que los clientes de su objeto contado de referencia llamen a métodos no constantes que podrían liberar los datos del objeto, entonces usar un puntero compartido es casi inútil.
Escribí "casi" en esa oración anterior porque el rendimiento puede ser una preocupación y "podría" estar justificado en casos excepcionales, pero también evitaría este escenario y buscaría todas las otras posibles soluciones de optimización yo mismo, como buscar seriamente al agregar otro nivel de indirección, evaluación perezosa, etc.
El código que existe más allá de su autor, o incluso publica su memoria del autor, que requiere suposiciones implícitas sobre el comportamiento, en particular el comportamiento sobre la vida útil de los objetos, requiere documentación clara, concisa y legible, ¡y muchos clientes no lo leerán de todos modos! La simplicidad casi siempre triunfa sobre la eficiencia, y casi siempre hay otras formas de ser eficiente. Si realmente necesita pasar valores por referencia para evitar la copia profunda mediante los constructores de copia de sus objetos contados de referencia (y el operador igual), entonces tal vez debería considerar formas de hacer que los datos copiados en profundidad sean punteros contados de referencia que pueden ser copiado rápidamente. (Por supuesto, ese es solo un escenario de diseño que podría no aplicarse a su situación).
fuente
Solía trabajar en un proyecto en el que el principio era muy sólido acerca de pasar punteros inteligentes por valor. Cuando me pidieron que hiciera un análisis de rendimiento, descubrí que para el incremento y la disminución de los contadores de referencia de los punteros inteligentes, la aplicación gasta entre el 4 y el 6% del tiempo de procesador utilizado.
Si desea pasar los indicadores inteligentes por valor solo para evitar problemas en casos extraños, como lo describe Daniel Earwicker, asegúrese de comprender el precio que paga por ello.
Si decide ir con una referencia, la razón principal para usar la referencia const es hacer posible tener una conversión ascendente implícita cuando necesita pasar un puntero compartido a un objeto de la clase que hereda la clase que usa en la interfaz.
fuente
Además de lo que dijo litb, me gustaría señalar que probablemente se deba pasar por la referencia constante en el segundo ejemplo, de esa manera está seguro de no modificarlo accidentalmente.
fuente
pasar por valor:
pasar por referencia:
pasar por puntero:
fuente
Cada pieza de código debe tener algún sentido. Si pasa un puntero compartido por valor en todas partes de la aplicación, esto significa "No estoy seguro de lo que está sucediendo en otros lugares, por lo tanto, prefiero la seguridad bruta ". Esto no es lo que yo llamo una buena señal de confianza para otros programadores que podrían consultar el código.
De todos modos, incluso si una función obtiene una referencia constante y no está "seguro", aún puede crear una copia del puntero compartido en el encabezado de la función, para agregar una referencia fuerte al puntero. Esto también podría verse como una pista sobre el diseño ("el puntero podría modificarse en otro lugar").
Así que sí, en mi opinión, el valor predeterminado debería ser " pasar por referencia constante ".
fuente