Pregunta

Si tengo una función que necesita para trabajar con un shared_ptr, no sería más eficaz para pasar una referencia a él (por lo que para evitar la copia de el shared_ptr objeto)?¿Cuáles son los posibles efectos secundarios negativos?Yo veo dos posibles casos:

1) dentro de la función se realiza 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 sólo es utilizado, como en

Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here  
{    
    ...  
    sp->do_something();  
    ...  
}  

Yo no puedo ver en ambos casos una buena razón para pasar el boost::shared_ptr<foo> por valor, en lugar de por referencia.Se pasa por valor sería sólo "temporalmente" el incremento de la cuenta de referencia debido a la copia y, a continuación, decremento que al salir el ámbito de la función.Estoy con vistas a algo?

Solo para aclarar, después de leer varias respuestas:Estoy perfectamente de acuerdo en el prematuro de optimización de preocupaciones, y yo siempre trato de primera-perfil-entonces-trabajo-en-el-puntos de acceso.Mi pregunta fue más desde una perspectiva puramente código técnico de-punto-de-vista, si usted sabe a qué me refiero.

¿Fue útil?

Solución

El objetivo de una instancia shared_ptr distinta es garantizar (en la medida de lo posible) que mientras este sp->do_something() esté dentro del alcance, el objeto al que apunta seguirá existiendo, porque su recuento de referencias 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 std::string, deshabilita esa garantía. Entonces, en su segundo caso:

Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here  
{    
    ...  
    sp->do_something();  
    ...  
}

¿Cómo sabe que Hi! no explotará debido a un puntero nulo?

Todo depende de lo que esté en esas secciones '...' del código. ¿Qué sucede 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 send_message para ese mismo objeto? ¿Y qué pasa si resulta ser el único msg distintivo restante de ese objeto? Adiós objeto, justo donde estás a punto de intentar usarlo.

Entonces, hay dos formas de responder esa pregunta:

  1. 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.

  2. Cambie el parámetro para que sea un objeto distinto en lugar de una referencia.

Consejos generales que se aplican aquí: no se moleste en hacer 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 marcará una diferencia significativa en el rendimiento.

Actualización para 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, por lo que en lugar de usar un shared_ptr & que probablemente se copie a medida que se pasa a varios lugares, usamos un std::shared_ptr<std::string> en una cadena:

void send_message(std::shared_ptr<std::string> msg)
{
    std::cout << (*msg.get()) << std::endl;
}

(Simplemente " 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, entonces no debe haber ningún mensaje anterior (la variable debe reiniciarse antes del envío). 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, una vez completado 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 <=> dos veces.

Ahora viene el Sr. Maintainer, que mira el código y piensa: Hola, ese parámetro para <=> es un <=>:

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)

¡Piense en la mejora del rendimiento que esto traerá! (No importa que estemos a punto de enviar un mensaje típicamente grande a través de algún canal, por lo que la mejora del rendimiento será tan pequeña que no se podrá medir).

Pero el verdadero problema 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 a <=> en un intento por evitar que el problema ocurra:

void send_message(const std::shared_ptr<std::string> &msg)
{
    if (msg == 0)
        return;

Pero, por supuesto, sigue adelante y se bloquea, porque <=> nunca es nulo cuando se llama a <=>.

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 mantienen 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 desea que una función pueda confiar en un <=> que sigue siendo no nulo en todo momento, es que la función asigne su propio <=> verdadero, en lugar de depender de una referencia a un <=>.

existente

La desventaja es que copiar un <=> no es gratis: incluso " lock-free " Las implementaciones tienen que usar una operación entrelazada para honrar las garantías de subprocesos. Por lo tanto, puede haber situaciones en las que un programa puede acelerarse significativamente cambiando un <=> en un <=>. Pero este no es un cambio que se pueda hacer de manera segura en todos los programas. Cambia el significado lógico del programa.

Tenga en cuenta que se produciría un error similar si utilizáramos <=> en lugar de <=>, 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 adicional de una cadena muy grande puede ser mucho más significativo que el costo de copiar un <=>, por lo que la compensación puede ser diferente.

Otros consejos

Me encontré en desacuerdo con la respuesta más votada, así que busqué 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_ptr's, las copias son caras "

Scott Meyers: " No hay nada especial en 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. La gente parece 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 barato pasarlo por valor. Tiene que ser copiado, y hay un costo asociado con eso ... es costoso pasarlo por valor, así que si puedo salir con la semántica adecuada en mi programa, lo pasaré por referencia a const o referencia en su lugar "

Herb Sutter: " siempre páselos por referencia a const, y muy ocasionalmente quizás porque sabes lo que llamaste podría modificar la cosa de la que obtuviste una referencia, tal vez entonces podrías pasar por valor ... si los copia como parámetros, oh Dios mío, casi nunca necesita aumentar ese recuento de referencias porque de todos modos se mantiene vivo, y debería pasarlo por referencia, así que haga 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 debes pasar shared_ptr's en absoluto quot; a menos que desee utilizar o manipular el puntero inteligente en sí, como compartir o transferir la propiedad. "

Aconsejaría en contra de esta práctica a menos que usted y los otros programadores con los que trabaje realmente, realmente sepan lo que están haciendo.

Primero, no tienes idea de cómo podría evolucionar la interfaz con tu clase y quieres 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 de usar incorrectamente. Programa a la defensiva: dificulta el uso incorrecto de la interfaz. Pasar por referencia solo va a invitar a problemas más adelante.

Segundo, no optimices hasta que sepas que esta clase en particular va a ser un problema. Perfil primero, y luego si su programa realmente necesita el impulso dado 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), sino que se preocupe por el diseño, las estructuras de datos, los algoritmos y el mantenimiento a largo plazo.

Sí, tomar una referencia está bien allí. No tiene intención de dar al método propiedad compartida; solo quiere trabajar con eso. También puede tomar una referencia para el primer caso, ya que la copia de todos modos. Pero para el primer caso, toma la propiedad. 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 qué está haciendo el cliente con él (podría almacenar un puntero y luego ocurre un gran estallido). Si luego resulta que es 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.

Es sensato pasar shared_ptr s por const&. Probablemente no causará problemas (excepto en el caso poco probable de que el boost::shared_ptr al que se hace referencia se elimine durante la llamada a la función, como lo detalla Earwicker) y probablemente sea más rápido si pasa muchos de estos. Recuerda; el valor predeterminado & es seguro para subprocesos, por lo que copiarlo incluye un incremento seguro para subprocesos.

Intente usar <=> en lugar de solo <=>, porque los objetos temporales no pueden pasarse por referencia no constante. (Aunque una extensión de idioma en MSVC le permite hacerlo de todos modos)

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);

Evitaría un " plain " referencia a menos que la función explícitamente pueda modificar el puntero.

A const & puede ser una micro-optimización sensata cuando se llaman funciones pequeñas, p. ej. para permitir más optimizaciones, como eliminar 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 haga una gran diferencia en la mayoría de los escenarios.

Generalmente, debe usar el estilo más simple a menos que tenga una razón para no hacerlo. Luego, use <=> de manera consistente o agregue un comentario sobre por qué si lo usa solo en algunos lugares.

Yo recomendaría pasar el puntero compartido por referencia constante: una semántica de que la función que se pasa con el puntero NO posee el puntero, lo cual es un lenguaje limpio para los desarrolladores.

El único inconveniente es que en los programas de múltiples hilos el objeto que apunta el puntero compartido se destruye en otro hilo. Por lo tanto, es seguro decir que usar la referencia constante del puntero compartido es seguro en un programa de subproceso único.

Pasar el puntero compartido por una referencia no constante es a veces peligroso; la razón es que las funciones de intercambio y restablecimiento pueden invocarse dentro de la función para destruir el objeto que aún se considera válido después de que la función regrese.

Supongo que no se trata de la optimización prematura: se trata de evitar el desperdicio innecesario de los ciclos de la CPU cuando tiene claro lo que quiere hacer y el idioma de codificación ha sido adoptado firmemente por sus colegas desarrolladores.

Solo mis 2 centavos :-)

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, debe conocer la semántica de pasar por referencia, referencia constante y valor y usarlo correctamente. Pero no hay absolutamente nada inherentemente malo en 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 sabe que sp.do_something() no explotará debido a un puntero colgante?

La verdad es que, shared_ptr o no, constante o no, esto podría suceder si tiene un defecto de diseño, como compartir directa o indirectamente la propiedad de sp entre hilos, haciendo mal uso de un objeto que hace delete this, usted tener una propiedad circular u otros errores de propiedad.

Una cosa que aún no he mencionado es que cuando pasas punteros compartidos por referencia, pierdes la conversión implícita que obtienes si quieres 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 cambia 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;
}

Asumiré que está familiarizado con optimización prematura y lo están solicitando con fines académicos o porque ha aislado un código preexistente que tiene un rendimiento bajo.

Pasar por referencia está bien

Pasar por referencia constante es mejor, y generalmente se puede usar, ya que no fuerza la constante en el objeto señalado.

no corre el 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 que solo un subproceso posee una pila de llamadas, por lo que la copia preexistente no desaparecerá.

Usar referencias es a menudo más eficiente por las razones que menciona, pero no está garantizado . Recuerde que desreferenciar un objeto también puede llevar trabajo. Su escenario ideal de uso de referencia sería si su estilo de codificación involucra muchas funciones pequeñas, donde el puntero se 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.

Suponiendo que no nos interese la corrección constante (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, ya que permitimos que el boost original :: shared_ptr controle su propia vida útil. 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 );
}

Del ejemplo anterior vemos que pasar sharedA por referencia permite que FooTakesReference restablezca 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 pueden utilizarse. Cuando inevitablemente aparece otro desarrollador e intenta aprovecharse de la frágil existencia de sharedA , se produce el caos. El afortunado desarrollador sharedB , sin embargo, se va temprano a casa ya que todo está bien en su mundo.

El código de seguridad, en este caso, supera con creces cualquier mejora en la velocidad que crea la copia. Al mismo tiempo, el 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.

Sandy escribió: & "; Parece que todos los pros y los contras aquí en realidad se pueden generalizar a CUALQUIER tipo pasado por referencia no solo shared_ptr. &";

Verdadero hasta cierto punto, pero el objetivo de usar shared_ptr es eliminar las preocupaciones relacionadas con la vida útil de los objetos y dejar que el compilador se encargue de eso. Si va a pasar un puntero compartido por referencia y permite que los clientes de su llamada de 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, como considerar seriamente 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 la vida útil de los objetos, requiere una 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 por 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).

Yo solía trabajar en un proyecto que al principio era muy fuerte acerca del paso de punteros inteligentes por valor.Cuando se me pidió hacer algunos análisis de rendimiento - he encontrado que para el incremento y decremento de los contadores de referencia de los punteros inteligentes la aplicación gasta entre un 4-6% de la utilizada tiempo de procesador.

Si desea pasar los punteros inteligentes por el valor justo para evitar tener problemas en raros casos, como se describe en Daniel Earwicker asegúrese de entender el precio que pagar por ello.

Si usted decide ir con una referencia a la principal razón para el uso constante de referencia es hacer posible el tener implícito upcasting cuando usted necesita para pasar compartido puntero a objeto de clase que hereda de la clase que uso en la interfaz.

Además de lo que dijo litb, me gustaría señalar que es probable que pase por referencia constante en el segundo ejemplo, de esa manera está seguro de que no lo modifica accidentalmente.

struct A {
  shared_ptr<Message> msg;
  shared_ptr<Message> * ptr_msg;
}
  1. 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;
    
  2. pasar por referencia:

    void set(shared_ptr<Message>& msg) {
     this->msg = msg; /// reference count will be added, because reference is just an alias.
     }
    
  3. pasar por puntero:

    void set(shared_ptr<Message>* msg) {
      this->ptr_msg = msg; /// reference count will not be added;
    }
    

Cada pieza de código debe tener algún sentido. Si pasa un puntero compartido por valor en todas partes en la aplicación, esto significa & Quot; ¡No estoy seguro de lo que está sucediendo en otro lugar, por lo tanto, estoy a favor de la seguridad en bruto quot ;. 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 usted está " inseguro " ;, aún puede crear una copia del puntero compartido en la cabecera de la función, para agregar una referencia fuerte al puntero. Esto también podría verse como una pista sobre el diseño (& Quot; el puntero podría modificarse en otro lugar & Quot;).

Entonces sí, IMO, el valor predeterminado debería ser " pasar por referencia constante " ;.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top