C++-pasa referencias a std:: shared_ptr o boost:: shared_ptr
pass-by-reference pass-by-value (17)
Aconsejaría en contra de esta práctica a menos que tú y los otros programadores con los que trabajas realmente, realmente sepan lo que estás haciendo.
En primer lugar, 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 hace que sea fácil usarlo incorrectamente. Programa defensivamente: haga que la interfaz sea difícil de usar de forma incorrecta. Pasar por referencia solo va a provocar problemas más adelante.
Segundo, no optimices hasta que sepas que esta clase particular va a ser un problema. Perfil primero, y luego si su programa realmente necesita el impulso dado pasando por referencia, entonces tal vez. De lo contrario, no se preocupe por las cosas pequeñas (es decir, las instrucciones N adicionales que se necesitan para pasar de valor) en lugar de preocuparse por el diseño, las estructuras de datos, los algoritmos y el mantenimiento a largo plazo.
Si tengo una función que necesita trabajar con un shared_ptr
, ¿no sería más eficiente pasarle una referencia (para evitar copiar el objeto shared_ptr
)? ¿Cuáles son los posibles efectos secundarios malos? Imagino dos posibles casos:
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 puedo ver en ambos casos una buena razón para pasar el boost::shared_ptr<foo>
por valor 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 totalmente de acuerdo con las preocupaciones sobre la optimización prematura, y siempre trato de hacer primero un perfil y luego trabajar en los hotspots. Mi pregunta era más desde un punto de vista de código puramente técnico, si sabes a qué me refiero.
Además de lo que dijo Litb, me gustaría señalar que es probable que pase por la referencia constante en el segundo ejemplo, de esa manera está seguro de que no lo modifique accidentalmente.
Asumiré que está familiarizado con la optimización prematura y lo está pidiendo para fines académicos o porque ha aislado algún código preexistente que tiene un bajo rendimiento.
Pasar por referencia está bien
Pasar por referencia constante es mejor, y generalmente se puede usar, ya que no fuerza la constidad en el objeto apuntado.
No está en riesgo de perder el puntero debido al uso de una referencia. Esa referencia es evidencia de que tiene una copia del puntero inteligente al principio de la pila y solo un subproceso posee una pila de llamadas, por lo que la copia preexistente no va a desaparecer.
El uso de referencias es a menudo más eficiente por las razones que menciona, pero no está garantizado . Recuerde que desreferenciar un objeto puede tomar trabajo también. Su escenario ideal de uso de referencia sería si su estilo de codificación implica muchas funciones pequeñas, donde el puntero pasa de la función a la función para funcionar antes de ser utilizado.
Siempre debe evitar almacenar su puntero inteligente como referencia. Su ejemplo de Class::take_copy_of_sp(&sp)
muestra el uso correcto para eso.
Cada pieza de código debe tener algún sentido. Si pasa un puntero compartido por el valor en todas partes de la aplicación, significa "No estoy seguro de lo que sucede en otro lado, por lo tanto, prefiero la seguridad sin formato ". Esto no es lo que llamo un buen signo 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 usted está "inseguro", aún puede crear una copia del puntero compartido al principio 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 cualquier otro lugar").
Entonces, sí, IMO, el valor predeterminado debe ser " pass by const reference ".
El objetivo de una instancia shared_ptr
es garantizar (en la medida de lo posible) que mientras este shared_ptr
esté dentro del alcance, el objeto al que apunta seguirá existiendo, ya que su recuento de referencia será al menos 1.
Class::only_work_with_sp(boost::shared_ptr<foo> sp)
{
// sp points to an object that cannot be destroyed during this function
}
Entonces, al usar una referencia a un shared_ptr
, deshabilita esa garantía. Entonces en tu segundo caso:
Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here
{
...
sp->do_something();
...
}
¿Cómo sabes que sp->do_something()
no explotará debido a un puntero nulo?
Todo depende de lo que hay en esas secciones ''...'' del código. ¿Qué shared_ptr
si llama algo durante el primer ''...'' que tiene el efecto secundario (en algún lugar de otra parte del código) de borrar un shared_ptr
para ese mismo objeto? ¿Y qué ocurre si resulta ser el único shared_ptr
distinto que shared_ptr
para ese objeto? Adiós objeto, justo donde estás a punto de probar y usarlo.
Entonces hay dos formas de responder esa pregunta:
Examine la fuente de todo su programa con mucho cuidado hasta que esté seguro de que el objeto no va a morir durante el cuerpo de la función.
Cambie el parámetro de nuevo para que sea un objeto distinto en lugar de una referencia.
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 medido de manera concluyente que el cambio que desea realizar hará una diferencia significativa al 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 una std::string
que probablemente se copie a medida que se pasa a varios lugares, usamos un shared_ptr
para una cadena:
void send_message(std::shared_ptr<std::string> msg)
{
std::cout << (*msg.get()) << std::endl;
}
(Simplemente "lo enviamos" a la consola para este ejemplo).
Ahora queremos agregar una función para recordar el mensaje anterior. Deseamos el siguiente comportamiento: debe existir una variable que contenga el último mensaje enviado, pero mientras se está enviando un mensaje, entonces no debe haber ningún mensaje anterior (la variable debe reiniciarse antes de enviarse). Entonces declaramos la nueva variable:
std::shared_ptr<std::string> previous_message;
Luego modificamos nuestra función de acuerdo con las reglas que especificamos:
void send_message(std::shared_ptr<std::string> msg)
{
previous_message = 0;
std::cout << *msg << std::endl;
previous_message = msg;
}
Entonces, antes de comenzar a enviar descartamos el mensaje anterior actual, y luego de que se complete el envío podemos almacenar el nuevo mensaje anterior. Todo bien. Aquí hay un código de prueba:
send_message(std::shared_ptr<std::string>(new std::string("Hi")));
send_message(previous_message);
Y como se esperaba, esto imprime Hi!
dos veces.
Ahora viene el Sr. Maintainer, que mira el código y piensa: Oye, ese parámetro para send_message
es un shared_ptr
:
void send_message(std::shared_ptr<std::string> msg)
Obviamente eso se puede cambiar a:
void send_message(const std::shared_ptr<std::string> &msg)
¡Piensa en la mejora del rendimiento que esto traerá! (No importa que estemos a punto de enviar un mensaje típicamente grande por algún canal, por lo que la mejora del rendimiento será tan pequeña que no podrá medirse).
Pero el verdadero problema es que ahora el código de prueba exhibirá un comportamiento indefinido (en versiones de depuración de Visual C ++ 2010, se bloquea).
El Sr. Maintainer está sorprendido por esto, pero agrega un cheque defensivo a send_message
en un intento de detener el problema:
void send_message(const std::shared_ptr<std::string> &msg)
{
if (msg == 0)
return;
Pero, por supuesto, sigue adelante y se cuelga, porque msg
nunca es nulo cuando se llama send_message
.
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 sostienen punteros entre sí, es fácil cometer el error y es difícil construir los casos de prueba necesarios para detectar el error.
La solución fácil, donde se desea que una función pueda confiar en un shared_ptr
continúe siendo no nulo en todo, es que la función asigne su propia shared_ptr
verdadera, en lugar de confiar en una referencia a un shared_ptr
existente.
La desventaja es que copiar un shared_ptr
no es gratis: incluso las implementaciones "sin bloqueo" tienen que usar una operación entrelazada para cumplir con las garantías de enhebrado. Por lo tanto, puede haber situaciones en las que un programa puede acelerarse significativamente al cambiar un shared_ptr
en un shared_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 std::shared_ptr<std::string>
std::string
en lugar de std::shared_ptr<std::string>
y en lugar de:
previous_message = 0;
para borrar el mensaje, dijimos:
previous_message.clear();
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 extra de una cadena muy grande puede ser mucho más significativo que el costo de copiar un shared_ptr
, por lo que la compensación puede ser diferente.
En el segundo caso, hacer esto es más simple:
Class::only_work_with_sp(foo &sp)
{
...
sp.do_something();
...
}
Puedes llamarlo como
only_work_with_sp(*sp);
Es sensato pasar shared_ptr
s por const&
. No es probable que cause problemas (excepto en el caso improbable de que el shared_ptr
referenciado se elimine durante la llamada a la función, según lo detalla Earwicker) y es probable que sea más rápido si pasa mucho de esto. Recuerda; el boost::shared_ptr
predeterminado boost::shared_ptr
es seguro para subprocesos, por lo que copiarlo incluye un incremento seguro de subprocesos.
Intente usar const&
lugar de simplemente &
, ya que los objetos temporales no pueden pasarse por referencia sin const. (Aunque una extensión de idioma en MSVC le permite hacerlo de todos modos)
Evitaría una referencia "simple" a menos que la función explícitamente pueda modificar el puntero.
Un const &
puede ser una micro-optimización sensata cuando se llaman funciones pequeñas, por ejemplo, para permitir optimizaciones adicionales, como la reducción de algunas condiciones. Además, el incremento / decremento, dado 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.
En general, debe usar el estilo más simple a menos que tenga una razón para no hacerlo. Luego, use el const &
consistente, o agregue un comentario de por qué si lo usa solo en algunos lugares.
Me encontré en desacuerdo con la respuesta más votados, así que fui en busca de opiniones expertas y aquí están. Desde http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2011-Scott-Andrei-and-Herb-Ask-Us-Anything
Herb Sutter: "cuando pasas shared_ptr''s, las copias son caras"
Scott Meyers: "No hay nada especial acerca de shared_ptr cuando se trata de pasarlo por valor o pasarlo por referencia. Use exactamente el mismo análisis que usa para cualquier otro tipo definido por el usuario. Las personas parecen tener esta percepción de que shared_ptr de alguna manera resuelve todos los problemas de gestión, y que debido a que es pequeño, es necesariamente económico pasar por valor. Tiene que ser copiado, y hay un costo asociado con eso ... es caro pasarlo por valor, así que si puedo salirse con la suya con la semántica adecuada en mi programa, voy a pasarlo por referencia a const o referencia en su lugar "
Herb Sutter: "siempre páselos por referencia a const, y muy ocasionalmente, tal vez porque sabes que lo que llamaste podría modificar lo que obtuviste como referencia, tal vez podrías pasar por valor ... si los copias como parámetros, oh Dios mío, casi nunca necesitarás superar ese número de referencia porque de todos modos se mantiene vivo, y deberías pasarlo por referencia, así que hazlo ".
Actualización: Herb ha ampliado sobre esto aquí: http://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/ , aunque la moraleja de la historia es que no se debe pasar shared_ptr en absoluto "a menos que desee usar o manipular el puntero inteligente en sí, como compartir o transferir la propiedad".
Parece que todos los pros y los contras aquí se pueden generalizar a CUALQUIER tipo pasado por referencia no solo shared_ptr. En mi opinión, debes saber la semántica de pasar por referencia, referencia y valor y usarla correctamente. Pero no hay absolutamente nada inherentemente incorrecto al pasar shared_ptr por referencia, a menos que piense que todas las referencias son malas ...
Para volver al ejemplo:
Class::only_work_with_sp( foo &sp ) //Again, no copy here
{
...
sp.do_something();
...
}
¿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 tienes un defecto de diseño, como compartir directa o indirectamente la propiedad de sp
entre hilos, usar un objeto que sí lo delete this
, tienes una propiedad circular u otra errores de propiedad.
Sí, tomar una referencia está bien allí. No tiene la intención de otorgarle al método propiedad compartida; solo quiere trabajar con eso. También podría tomar una referencia para el primer caso, ya que lo copia de todos modos. Pero para el primer caso, toma posesión. Existe este truco para copiarlo solo una vez:
void ClassA::take_copy_of_sp(boost::shared_ptr<foo> sp) {
m_sp_member.swap(sp);
}
También debe copiar cuando lo devuelva (es decir, no devolver una referencia). Debido a que su clase no sabe lo que el cliente está haciendo con él (podría almacenar un puntero a él y luego sucede el Big Bang). Si luego resulta ser un cuello de botella (¡primer perfil!), Aún puede devolver una referencia.
Editar : por supuesto, como otros señalan, 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, simplemente pase por valor.
Sandy escribió: "Parece que todos los pros y los contras aquí se pueden generalizar a CUALQUIER tipo pasado por referencia no solo shared_ptr".
Es cierto hasta cierto punto, pero el objetivo de usar shared_ptr es eliminar las preocupaciones sobre la duración de los objetos y permitir que el compilador maneje eso por usted. Si va a pasar un puntero compartido por referencia y permite a los clientes de su referencia de objeto contado invocar métodos no const que podrían liberar los datos del objeto, entonces usar un puntero compartido es casi inútil.
Escribí "casi" en la frase anterior porque el rendimiento puede ser una preocupación, y "podría" justificarse en casos raros, pero también evitaría este escenario yo mismo y buscaría todas las otras posibles soluciones de optimización, como mirar 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 publicar su memoria de autor, que requiere suposiciones implícitas sobre el comportamiento, en particular el comportamiento sobre el tiempo de vida de los objetos, requiere documentación clara, concisa y legible, ¡y muchos clientes no lo leerán de todos modos! La simplicidad casi siempre supera la eficiencia, y casi siempre hay otras formas de ser eficiente. Si realmente necesita pasar valores por referencia para evitar la copia profunda de los constructores de copia de sus objetos contados de referencia (y el operador igual), entonces quizás deba considerar formas de hacer que los datos profundamente copiados sean punteros de referencia contados 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).
Solía trabajar en un proyecto que el principio era muy fuerte acerca de pasar punteros inteligentes por valor. Cuando me pidieron que hiciera algunos análisis de rendimiento, encontré que para el incremento y la disminución de los contadores de referencia de los punteros inteligentes, la aplicación gasta entre un 4% y un 6% del tiempo de procesador utilizado.
Si desea pasar los punteros inteligentes por valor solo para evitar problemas en casos extraños, como se describe en Daniel Earwicker, asegúrese de comprender el precio que paga por ello.
Si decide usar una referencia, la razón principal para usar la referencia const es hacer posible la actualización implícita cuando necesite pasar un puntero compartido a un objeto de la clase que hereda la clase que utiliza en la interfaz.
Suponiendo que no nos preocupa la const correctness (o más, queremos 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 original :: shared_ptr controle su propia vida. Considere los resultados del siguiente código ...
void FooTakesReference( boost::shared_ptr< int > & ptr )
{
ptr.reset(); // We reset, and so does sharedA, memory is deleted.
}
void FooTakesValue( boost::shared_ptr< int > ptr )
{
ptr.reset(); // Our temporary is reset, however sharedB hasn''t.
}
void main()
{
boost::shared_ptr< int > sharedA( new int( 13 ) );
boost::shared_ptr< int > sharedB( new int( 14 ) );
FooTakesReference( sharedA );
FooTakesValue( sharedB );
}
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 se pueden usar. Cuando inevitablemente aparece otro desarrollador e intenta aprovecharse de la existencia frágil de sharedA , se produce el caos. El afortunado desarrollador de SharedB , sin embargo, se va a casa temprano ya que todo está bien en su mundo.
La seguridad del código, en este caso, supera con creces cualquier mejora en la 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.
Una cosa que no he mencionado aún es que cuando pasa punteros compartidos por referencia, pierde la conversión implícita que obtiene si desea pasar un puntero compartido de clase derivado a través de una referencia a un puntero compartido de clase base.
Por ejemplo, este código producirá un error, pero funcionará si cambia la test()
para que el puntero compartido no se pase por referencia.
#include <boost/shared_ptr.hpp>
class Base { };
class Derived: public Base { };
// ONLY instances of Base can be passed by reference. If you have a shared_ptr
// to a derived type, you have to cast it manually. If you remove the reference
// and pass the shared_ptr by value, then the cast is implicit so you don''t have
// to worry about it.
void test(boost::shared_ptr<Base>& b)
{
return;
}
int main(void)
{
boost::shared_ptr<Derived> d(new Derived);
test(d);
// If you want the above call to work with references, you will have to manually cast
// pointers like this, EVERY time you call the function. Since you are creating a new
// shared pointer, you lose the benefit of passing by reference.
boost::shared_ptr<Base> b = boost::dynamic_pointer_cast<Base>(d);
test(b);
return 0;
}
Yo recomendaría pasar puntero compartido por referencia constante - una semántica que la función que se pasa con el puntero NO posee el puntero, que es una expresión limpia para los desarrolladores.
El único inconveniente es que en los programas de múltiples hilos el objeto apuntado por el puntero compartido se destruye en otro hilo. Por lo tanto, es seguro decir que el uso de la referencia constante del puntero compartido es seguro en el programa de un solo subproceso.
Pasar el puntero compartido por referencia no constante a veces es peligroso; la razón son las funciones de intercambio y restablecimiento que la función puede invocar en el interior para destruir el objeto que todavía se considera válido después de que la función retorna.
No se trata de una optimización prematura, supongo, se trata de evitar el desperdicio innecesario de ciclos de CPU cuando tiene claro lo que quiere hacer y la expresión de codificación ha sido adoptada firmemente por sus compañeros desarrolladores.
Solo mis 2 centavos :-)
struct A {
shared_ptr<Message> msg;
shared_ptr<Message> * ptr_msg;
}
pasar por valor:
void set(shared_ptr<Message> msg) { this->msg = msg; /// create a new shared_ptr, reference count will be added; } /// out of method, new created shared_ptr will be deleted, of course, reference count also be reduced;
pasar por referencia:
void set(shared_ptr<Message>& msg) { this->msg = msg; /// reference count will be added, because reference is just an alias. }
Pase por el puntero:
void set(shared_ptr<Message>* msg) { this->ptr_msg = msg; /// reference count will not be added; }