Pasar punteros compartidos como argumentos

88

Si declaro un objeto envuelto en un puntero compartido:

std::shared_ptr<myClass> myClassObject(new myClass());

luego quería pasarlo como argumento a un método:

DoSomething(myClassObject);

//the called method
void DoSomething(std::shared_ptr<myClass> arg1)
{
   arg1->someField = 4;
}

¿Lo anterior simplemente incrementa el recuento de referencias de shared_pt y todo está bien? ¿O deja un puntero colgando?

¿Todavía se supone que debes hacer esto ?:

DoSomething(myClassObject.Get());

void DoSomething(std::shared_ptr<myClass>* arg1)
{
   (*arg1)->someField = 4;
}

Creo que la segunda forma puede ser más eficiente porque solo tiene que copiar 1 dirección (a diferencia de todo el puntero inteligente), pero la primera forma parece más legible y no anticipo empujar los límites de rendimiento. Solo quiero asegurarme de que no haya nada peligroso al respecto.

Gracias.

Steve H
fuente
14
const std::shared_ptr<myClass>& arg1
Capitán Obvlious
3
La segunda forma está rota, la primera es idiomática si realmente necesita que su función comparta la propiedad. Pero, ¿ DoSomethingrealmente es necesario compartir la propiedad? Parece que debería tomar una referencia en su lugar ...
ildjarn
8
@SteveH: No es así, pero ¿por qué su función debería forzar una semántica de propiedad de objeto especial en sus llamadores si en realidad no los necesita? Tu función no hace nada que no sea mejor que void DoSomething(myClass& arg1).
ildjarn
2
@SteveH: El propósito completo de los punteros inteligentes es manejar los problemas de propiedad de objetos fuera de lo común; si no los tiene, no debería usar punteros inteligentes en primer lugar. ; -] En cuanto a shared_ptr<>específicamente, debe pasar por valor para poder compartir la propiedad.
ildjarn
4
No relacionado: en general, std::make_sharedno solo es más eficiente, sino también más seguro que el std::shared_ptrconstructor.
R. Martinho Fernandes

Respuestas:

171

Quiero pasar un puntero compartido a una función. ¿Me puede ayudar con eso?

Claro, puedo ayudarte con eso. Supongo que tiene cierta comprensión de la semántica de propiedad en C ++. ¿Es eso cierto?

Sí, me siento razonablemente cómodo con el tema.

Bueno.

Ok, solo puedo pensar en dos razones para shared_ptrargumentar:

  1. La función quiere compartir la propiedad del objeto;
  2. La función realiza alguna operación que trabaja específicamente en shared_ptr s.

Cual te interesa?

Estoy buscando una respuesta general, así que estoy interesado en ambos. Sin embargo, tengo curiosidad por saber a qué te refieres con el caso n. ° 2.

Ejemplos de tales funciones incluyen std::static_pointer_cast comparadores personalizados o predicados. Por ejemplo, si necesita encontrar todos los shared_ptr únicos de un vector, necesita dicho predicado.

Ah, cuando la función realmente necesita manipular el puntero inteligente.

Exactamente.

En ese caso, creo que deberíamos pasar por referencia.

Si. Y si no cambia el puntero, desea pasar por referencia constante. No es necesario copiar, ya que no es necesario compartir la propiedad. Ese es el otro escenario.

Ok lo tengo. Hablemos del otro escenario.

¿En el que compartes la propiedad? Okay. ¿Cómo se comparte la propiedad conshared_ptr ?

Copiándolo.

Entonces la función necesitará hacer una copia de a shared_ptr, ¿correcto?

Obviamente. Entonces, ¿lo paso por una referencia a const y lo copio en una variable local?

No, eso es una pesimización. Si se pasa por referencia, la función no tendrá más remedio que realizar la copia manualmente. Si se pasa por valor, el compilador elegirá la mejor opción entre una copia y un movimiento y la realizará automáticamente. Entonces, pase por valor.

Buen punto. Debo recordar eso " ¿Quieres velocidad? Pasar por valor " con más frecuencia.

Espere, ¿y si la función almacena el shared_ptr en una variable miembro, por ejemplo? ¿No será una copia redundante?

La función puede simplemente mover el shared_ptrargumento a su almacenamiento. Mover unshared_ptr es barato porque no cambia ningún recuento de referencias.

Ah, buena idea.

Pero estoy pensando en un tercer escenario: ¿qué pasa si no quieres manipular shared_ptrni compartir la propiedad?

En ese caso, shared_ptres completamente irrelevante para la función. Si desea manipular el pointee, tome un pointee y deje que las personas que llaman elijan qué semántica de propiedad quieren.

¿Y debería tomar el pointee por referencia o por valor?

Se aplican las reglas habituales. Los punteros inteligentes no cambian nada.

Pasar por valor si voy a copiar, pasar por referencia si quiero evitar una copia.

Derecha.

Hmm. Creo que olvidaste otro escenario. ¿Qué sucede si quiero compartir la propiedad, pero solo dependiendo de una determinada condición?

Ah, un caso extremo interesante. No espero que eso suceda a menudo. Pero cuando suceda, puede pasar por valor e ignorar la copia si no la necesita, o pasar por referencia y hacer la copia si la necesita.

Me arriesgo a una copia redundante en la primera opción y pierdo un movimiento potencial en la segunda. ¿No puedo comerme el pastel y tenerlo también?

Si se encuentra en una situación en la que eso realmente importa, puede proporcionar dos sobrecargas, una tomando una referencia de valor constante y otra tomando una referencia de valor r. Uno copia, el otro se mueve. Una plantilla de función de reenvío perfecto es otra opción.

Creo que cubre todos los escenarios posibles. Muchas gracias.

R. Martinho Fernandes
fuente
2
@Jon: ¿Para qué? Esto es todo acerca debo hacer una o debería hacerlo B . Creo que todos sabemos cómo pasar objetos por valor / referencia, ¿no?
sbi
9
Porque una prosa como "¿Eso significa que lo pasaré por una referencia a const para hacer la copia? No, eso es una pesimización" es confuso para un principiante. Pasar por una referencia a const no hace una copia.
Jon
1
@Martinho No estoy de acuerdo con la regla "Si quieres manipular el pointee, haz un pointee". Como se indica en esta respuesta, no tomar posesión temporal de un objeto puede ser peligroso. En mi opinión, el valor predeterminado debería ser pasar una copia para la propiedad temporal para garantizar la validez del objeto durante la duración de la llamada a la función.
radman
@radman Ningún escenario en la respuesta a la que enlaza aplica esa regla, por lo que es completamente irrelevante. Por favor escríbame un SSCCE donde pase por referencia de a shared_ptr<T> ptr;(es decir void f(T&), llamado con f(*ptr)) y el objeto no sobrevive a la llamada. Alternativamente, escriba uno en el que pase por el valor void f(T)y los problemas.
R. Martinho Fernandes
@Martinho echa un vistazo a mi código de ejemplo para ver el ejemplo correcto e independiente. El ejemplo es para void f(T&)hilos y los involucra. Creo que mi problema es que el tono de su respuesta desalienta la transferencia de propiedad de shared_ptr's, que es la forma más segura, intuitiva y menos complicada de usarlos. Estaría extremadamente en contra de aconsejar a un principiante que extraiga una referencia a los datos propiedad de un shared_ptr <>, las posibilidades de uso indebido son grandes y el beneficio es mínimo.
radman
22

Creo que la gente tiene miedo innecesariamente de usar punteros sin procesar como parámetros de función. Si la función no va a almacenar el puntero o afectará su vida útil, un puntero sin formato funciona igual de bien y representa el mínimo común denominador. Considere, por ejemplo, cómo pasaría a unique_ptra una función que toma a shared_ptrcomo parámetro, ya sea por valor o por referencia constante.

void DoSomething(myClass * p);

DoSomething(myClass_shared_ptr.get());
DoSomething(myClass_unique_ptr.get());

Un puntero sin formato como parámetro de función no le impide usar punteros inteligentes en el código de llamada, donde realmente importa.

Mark Ransom
fuente
8
Si está utilizando un puntero, ¿por qué no utilizar simplemente una referencia? DoSomething(*a_smart_ptr)
Xeo
5
@Xeo, tienes razón, eso sería aún mejor. Sin embargo, a veces debe permitir la posibilidad de un puntero NULL.
Mark Ransom
1
Si usted tiene una convención de usar punteros primas como parámetros de salida es más fácil de detectar en código de llamada que una variable puede cambiar, por ejemplo: comparar fun(&x)a fun(x). En el último ejemplo, xpodría pasarse por valor o como una ref constante. Como se indicó anteriormente, también le permitirá pasar nullptrsi no está interesado en la salida ...
Andreas Magnusson
2
@AndreasMagnusson: Esa convención de puntero como parámetro de salida puede dar una falsa sensación de seguridad cuando se trata de punteros pasados ​​desde otra fuente. Porque en ese caso la llamada es divertida (x) y * x se modifica y la fuente no tiene & x para advertirle.
Zan Lynx
¿Qué pasa si la función llamada sobrevive al alcance de la persona que llama? Existe la posibilidad de que se haya llamado al destructor del ptr compartido, y ahora la función intentará acceder a la memoria eliminada, lo que dará como resultado UB
Sridhar Thiagarajan
4

Sí, la idea completa sobre un shared_ptr <> es que varias instancias pueden contener el mismo puntero sin procesar y la memoria subyacente solo se liberará cuando se destruya la última instancia de shared_ptr <>.

Evitaría un puntero a shared_ptr <> ya que eso anula el propósito, ya que ahora está tratando con raw_pointers nuevamente.

R Samuel Klatchko
fuente
2

Pasar por valor en su primer ejemplo es seguro, pero hay un modismo mejor. Pase por la referencia constante cuando sea posible; yo diría que sí incluso cuando se trata de punteros inteligentes. Su segundo ejemplo no está exactamente roto pero es muy !???. Tonto, no logra nada y derrota parte del punto de los punteros inteligentes, y te va a dejar en un mundo lleno de errores cuando intentas desreferenciar y modificar las cosas.

djechlin
fuente
3
Nadie aboga por pasar por puntero, si te refieres a puntero en bruto. Pasar por referencia si no hay disputa de propiedad es ciertamente idiomático. El punto es, shared_ptr<>específicamente, que debe hacer una copia para poder compartir la propiedad; si tiene una referencia o puntero a un, shared_ptr<>entonces no está compartiendo nada y está sujeto a los mismos problemas de por vida que una referencia o puntero normal.
ildjarn
2
@ildjarn: Pasar una referencia constante está bien en este caso. Se shared_ptrgarantiza que el original de la persona que llama durará más que la llamada a la función, por lo que la función es segura al usar shared_ptr<> const &. Si necesita almacenar el puntero para un momento posterior, tendrá que copiar (en este punto se debe hacer una copia, en lugar de guardar una referencia), pero no hay necesidad de incurrir en el costo de la copia a menos que necesite hacerlo. eso. Yo iría por pasar una referencia constante en este caso ....
David Rodríguez - dribeas
3
@David: " Pasar una referencia constante está bien en este caso " . No en ninguna API sensata, ¿por qué una API exigiría un tipo de puntero inteligente del que ni siquiera se está aprovechando en lugar de simplemente tomar una referencia constante normal? Eso es casi tan malo como la falta de corrección constante. Si su punto es que técnicamente no está perjudicando nada, entonces ciertamente estoy de acuerdo, pero no creo que sea lo correcto.
ildjarn
2
... si, por otro lado, la función no necesita extender la vida útil del objeto, entonces puede simplemente eliminar el shared_ptrde la interfaz y pasar una constreferencia simple ( ) al objeto puntiagudo.
David Rodríguez - dribeas
3
@David: ¿Qué pasa si su consumidor de API no está usando shared_ptr<>para empezar? Ahora su API es realmente solo un dolor de cabeza, ya que obliga a la persona que llama a cambiar inútilmente la semántica de la vida útil de su objeto solo para usarla. No veo nada que valga la pena defender en eso, aparte de decir "técnicamente no daña nada". No veo nada controvertido sobre 'si no necesita la propiedad compartida, no la use shared_ptr<>'.
ildjarn
0

en su función DoSomething , está cambiando un miembro de datos de una instancia de clase, myClass por lo que lo que está modificando es el objeto administrado (puntero sin procesar), no el objeto (shared_ptr). Lo que significa que en el punto de retorno de esta función, todos los punteros compartidos al puntero sin procesar administrado verán su miembro de datos: myClass::someFieldcambiado a un valor diferente.

en este caso, está pasando un objeto a una función con la garantía de que no lo está modificando (hablando de shared_ptr, no del objeto de propiedad).

El modismo para expresar esto es a través de: una referencia constante, así

void DoSomething(const std::shared_ptr<myClass>& arg)

Asimismo, le está asegurando al usuario de su función que no está agregando otro propietario a la lista de propietarios del puntero sin procesar. Sin embargo, mantuvo la posibilidad de modificar el objeto subyacente apuntado por el puntero sin formato.

AVISO: Lo que significa que, si de alguna manera alguien llama shared_ptr::resetantes de que usted llame a su función, y en ese momento es el último shared_ptr que posee el raw_ptr, entonces su objeto será destruido y su función manipulará un puntero colgando al objeto destruido. ¡¡¡MUY PELIGROSO!!!

hombre orgánico
fuente