2008-09-25 6 views
69

Mientras refactoreo de algunos códigos encontré algunos métodos getter que devuelven una cadena std ::. Algo como esto, por ejemplo:Devolviendo una referencia constante a un objeto en lugar de una copia

class foo 
{ 
private: 
    std::string name_; 
public: 
    std::string name() 
    { 
     return name_; 
    } 
}; 

Sin duda, el comprador sería mejor devolver un const std::string&? El método actual es devolver una copia que no es tan eficiente. ¿Devolvería una referencia const en su lugar causaría algún problema?

+0

Quién dijo que no era eficiente. Se ha trabajado mucho en std :: string para que esta operación sea eficiente. Hay una diferencia entre pasar una referencia y un strincg, pero la diferencia real suele ser insignificante. –

+11

@Loki: nada dice que será eficiente, y de hecho, la versión más reciente de C++ dice que probablemente lo sea. la única optimización que haría que ese código fuera razonablemente eficiente sería una std :: string contada por referencia. Sin embargo, muchas implementaciones de STL no hacen recuento de referencias, ya que en las CPU multinúcleo modernas es mucho más lento administrar recuentos de referencia de lo que se podría pensar. Entonces sí, devolver una copia es mucho más lento. Ni GCC ni STL de Microsoft han hecho recuentos de referencia en std :: string en muchos años. –

+3

@seanmiddleditch: Mida siempre antes de asumir (eso es todo lo que digo). También hay muchas otras optimizaciones que el compilador puede aplicar: Inlinear y copiar elisión serían dos que reducen el costo a cero. –

Respuesta

51

La única forma en que esto puede causar un problema es si la persona que llama almacena la referencia, en lugar de copiar la cadena, e intenta utilizarla después de que se destruye el objeto. De esta manera:

foo *pFoo = new foo; 
const std::string &myName = pFoo->getName(); 
delete pFoo; 
cout << myName; // error! dangling reference 

Sin embargo, dado que su función existente devuelve una copia, no se romperá ninguno de los códigos existentes.

+0

No rompería el código existente, pero no obtendría ninguna de las ventajas de rendimiento. Si los problemas fueran tan fáciles de detectar como en su ejemplo, estaría bien, pero a menudo puede ser mucho más complicado ... por ejemplo, si tiene el vector y push_back, causando un cambio de tamaño, etc.optimización prematura, etc., etc. – tfinniga

+0

Por supuesto, obtiene una ventaja de rendimiento. Si devuelve el objeto, paga el costo de crear una copia adicional del objeto, el temporal que se devuelve. No paga ese costo si devuelve una referencia. – Dima

+6

@Dima: es probable que la mayoría de los compiladores realicen una optimización del valor de retorno con nombre en este caso. –

4

Lo cambiaría para devolver const std :: string &. La persona que llama probablemente realice una copia del resultado de todos modos si no cambia todo el código de llamada, pero no presentará ningún problema.

Una posible arruga surge si tiene múltiples hilos llamando al nombre(). Si devuelve una referencia, pero luego cambia el valor subyacente, el valor de la persona que llama cambiará. Pero el código existente no parece seguro para subprocesos de todos modos.

Eche un vistazo a la respuesta de Dima para un posible problema relacionado pero poco probable.

0

Depende de lo que necesite hacer. Tal vez quiera que todos los que llaman modifiquen el valor devuelto sin cambiar la clase. Si devuelve la referencia constante que no volará.

Por supuesto, el siguiente argumento es que la persona que llama podría hacer su propia copia. Pero si sabe cómo se usará la función y sabe que sucede de todos modos, entonces quizás hacerlo le ahorrará un paso más adelante en el código.

+0

No creo que sea un problema. Una persona que llama puede hacer "std :: string s = aFoo.name()", y s será una copia mutable. –

3

Es concebible que pueda romper algo si la persona que llama realmente desea una copia, porque estaban a punto de alterar el original y deseaban conservar una copia. Sin embargo, es mucho más probable que, de hecho, solo devuelva una referencia constante.

Lo más fácil de hacer es probarlo y luego probarlo para ver si todavía funciona, siempre que tenga algún tipo de prueba que pueda ejecutar. De lo contrario, me centraría en escribir primero la prueba, antes de continuar con la refactorización.

1

Las probabilidades son bastante buenas de que el uso típico de esa función no se rompa si cambia a una referencia constante.

Si todo el código que llama a esa función está bajo su control, simplemente haga el cambio y vea si el compilador se queja.

0

Normalmente devuelvo const & a menos que no pueda. QBziZ da un ejemplo de dónde este es el caso. Por supuesto, QBziZ también afirma que std :: string tiene una semántica de copiar y escribir, que rara vez se cumple hoy en día, ya que COW implica una gran sobrecarga en un entorno de subprocesos múltiples. Al devolver const & le pones la responsabilidad a la persona que llama para hacer lo correcto con la cadena en su extremo. Pero dado que se trata de un código que ya está en uso, probablemente no debería cambiarlo a menos que el perfil demuestre que la copia de esta cadena está causando problemas de rendimiento masivo. Luego, si decides cambiarlo, deberás probar a fondo para asegurarte de que no hayas roto nada. Esperemos que los otros desarrolladores con los que trabajas no hagan cosas incompletas como en la respuesta de Dima.

2

¿Importa? Tan pronto como utilice un compilador de optimización moderno, las funciones que regresen por valor no implicarán una copia a menos que se requiera semánticamente.

Ver the C++ lite FAQ en esto.

+0

La optimización del retorno por valor es tan sofisticada que usaría un puntero/referencia al valor del miembro original i.o. de una copia? No creo que este sea el caso. La optimización de Rbv se saltea algunos pasos al devolver un valor, pero aún así llamaría al copiador de cstring, aunque la versión in situ. – QBziZ

16

Uno de los problemas para el regreso referencia constante sería si el código de usuario algo como:

const std::string & str = myObject.getSomeString() ; 

Con un retorno std::string, el objeto temporal permanecería vivo y unido a str str hasta que se sale del ámbito.

Pero, ¿qué ocurre con un const std::string &? Mi conjetura es que tendríamos una referencia constante a un objeto que podría morir cuando su objeto primario se desasigna:

MyObject * myObject = new MyObject("My String") ; 
const std::string & str = myObject->getSomeString() ; 
delete myObject ; 
// Use str... which references a destroyed object. 

Así que mi preferencia va a la devolución referencia constante (porque, de todos modos, estoy más cómodo con enviar una referencia que esperar que el compilador optimice el extra temporal), siempre que se respete el siguiente contrato: "si lo quiere más allá de la existencia de mi objeto, lo copian antes de la destrucción de mi objeto"

10

Algunas implementaciones de std :: string share memory con semántica copy-on-write, por lo que return-by-value puede ser casi tan eficiente como return-by-reference y no tiene que wo rry sobre los problemas de la vida útil (el tiempo de ejecución lo hace por usted).

Si está preocupado por el rendimiento, entonces compare (< = ¡¡¡no puedo hacer suficiente hincapié en eso) !!! Pruebe ambos enfoques y mida la ganancia (o la falta de ella). Si uno es mejor y realmente te importa, entonces úsalo. Si no, entonces prefiera el valor por valor de la protección que ofrece contra problemas de por vida mencionados por otras personas.

Ya sabes lo que dicen de hacer suposiciones ...

+0

En su lugar, tiene que preocuparse por la seguridad del hilo al modificar cadenas no relacionadas :) – bk1e

+10

Heh. Lo que da el tiempo de ejecución, el multithreading se quita. :) – rlerallut

+1

Por curiosidad: ¿implementa g ++ std :: string con semántica de copiado-escritura? – Frank

7

bien, así que las diferencias entre devolviendo una copia y devolver la referencia son:

  • Rendimiento: Volviendo la referencia puede o no ser más rápido; depende de cómo se implemente std::string mediante la implementación de su compilador (como han señalado otros). Pero incluso si devuelve la referencia de la asignación después de la llamada a la función por lo general implica una copia, como en std::string name = obj.name();

  • Seguridad: Volviendo la referencia puede o no causar problemas (de referencia) colgando. Si los usuarios de su función no saben lo que están haciendo, almacenar la referencia como referencia y usarla después de que el objeto que proporciona el suministro quede fuera del alcance, entonces hay un problema.

Si quieres que rápido y seguro uso impulso :: shared_ptr. Su objeto puede almacenar internamente la cadena como shared_ptr y devolver un shared_ptr. De esta forma, no se copiará el objeto en marcha y siempre es seguro (a menos que los usuarios extraigan el puntero sin procesar con get() y hagan cosas con él después de que su objeto salga del alcance).

+4

nota: desde C++ 11 hay 'std :: shared_ptr' –

25

En realidad, otra cuestión específicamente con la devolución de una cadena no por referencia, es el hecho de que std::string proporciona acceso a través de puntero a un interno const char* a través del método c_str(). Esto me ha causado muchas horas de dolor de cabeza de depuración. Por ejemplo, digamos que quiero obtener el nombre de foo y pasarlo a JNI para construir una jstring para pasar a Java más adelante, y que name() devuelve una copia y no una referencia. Yo podría escribir algo como esto:

foo myFoo = getFoo(); // Get the foo from somewhere. 
const char* fooCName = foo.name().c_str(); // Woops! foo.name() creates a temporary that's destructed as soon as this line executes! 
jniEnv->NewStringUTF(fooCName); // No good, fooCName was released when the temporary was deleted. 

Si la persona que llama se va a hacer este tipo de cosas, podría ser mejor usar algún tipo de puntero inteligente, o una referencia constante, o por lo menos tener un encabezado de comentario de advertencia desagradable sobre su método foo.name(). Menciono JNI porque los antiguos programadores de Java podrían ser particularmente vulnerables a este tipo de encadenamiento de métodos que, de lo contrario, puede parecer inofensivo.

+2

Maldición, buena captura. Aunque creo que C++ 11 respetó la duración de los temporales para hacer que este código sea válido para cualquier compilador compatible. –

+0

@MarkMcKenna Eso es bueno saber. Hice un poco de búsqueda, y esta respuesta parece indicar que todavía debe tener un poco de cuidado, incluso con C++ 11: http://stackoverflow.com/a/2507225/13140 –

0

Devolver una referencia a un miembro expone la implementación de la clase. Eso podría evitar el cambio de clase. Puede ser útil para métodos privados o protegidos en caso de que se necesite la optimización. What should a C++ getter return

Cuestiones relacionadas