2009-02-26 20 views
5

Este es un problema de asignación de memoria que nunca he entendido realmente.¿Cuándo no es una buena idea pasar por referencia?

 
void unleashMonkeyFish() 
{ 
    MonkeyFish * monkey_fish = new MonkeyFish(); 
    std::string localname = "Wanda"; 
    monkey_fish->setName(localname); 
    monkey_fish->go(); 
} 

En el código anterior, he creado un objeto MonkeyFish en el montón, asignándole un nombre, y luego desataron que sobre el mundo. Digamos que la propiedad de la memoria asignada se ha transferido al objeto MonkeyFish, y solo MonkeyFish decidirá cuándo morirá y se eliminará.

Ahora, cuando defino el miembro de datos "nombre" dentro de la clase MonkeyFish, puedo elegir uno de los siguientes:

 
std::string name; 
std::string & name; 

Cuando defino el prototipo de la función setName() dentro de la clase MonkeyFish , Puedo elegir uno de los siguientes:

 
void setName(const std::string & parameter_name); 
void setName(const std::string parameter_name); 

Quiero ser capaz de minimizar las copias de cadenas. De hecho, quiero eliminarlos por completo si puedo. Entonces, parece que debería pasar el parámetro por referencia ... ¿verdad?

Lo que me molesta es que parece que mi variable de nombre local va a quedar fuera del alcance una vez que se completa la función unleashMonkeyFish(). ¿Eso significa que estoy obligado a pasar el parámetro por copia? ¿O puedo pasarlo por referencia y "salirse con la suya" de alguna manera?

Básicamente, quiero evitar estos escenarios:

  1. no quiero para establecer el nombre del MonkeyFish, sólo para que la memoria de la cadena nombre_local desaparecerá cuando la función unleashMonkeyFish() termina. (Esto parece que sería muy malo.)
  2. No quiero copiar la cadena si puedo evitarlo.
  3. yo preferiría no nombre_local nueva

¿Qué combinación prototipo y los datos miembro debería usar?

ACLARACIÓN: Varias respuestas sugirieron usar la palabra clave estática para asegurarse de que la memoria no se desajuste automáticamente cuando finalice unMonkeyFish(). Dado que el objetivo final de esta aplicación es liberar N MonkeyFish (todos los cuales deben tener nombres únicos) esta no es una opción viable. (Y sí, MonkeyFish - ser criaturas volubles - a menudo cambian sus nombres, en algún momento varias veces en un solo día.)

EDITAR: Greg Hewgil ha señalado que es ilegal para almacenar la variable nombre como referencia, ya que no se está configurando en el constructor. Dejo el error en la pregunta tal como está, ya que creo que mi error (y la corrección de Greg) podría ser útil para alguien que vea este problema por primera vez.

+0

¿Ha perfilado y determinado que esto es un problema? – CTT

+0

@CTT - No, no lo hice. Sin embargo, parece ineficaz copiar la cadena si puedo evitarla. En general, quiero evitar copiar lo más posible. – Runcible

+0

"monkey_fish" está en el montón, no en la pila. –

Respuesta

6

Una forma de hacer esto es tener su cadena

std::string name; 

A medida que la técnica de los miembros de su objeto.Y luego, en la función unleashMonkeyFish crear una cadena como lo hizo, y pasarlo por referencia como usted mostró

void setName(const std::string & parameter_name) { 
    name = parameter_name; 
} 

Se hará lo que usted quiere - la creación de una copia para copiar la cadena en los datos de miembros . No es como si tuviera que volver a asignar un nuevo búfer internamente si asigna otra cadena. Probablemente, la asignación de una nueva cadena simplemente copia algunos bytes. std :: string tiene la capacidad de reservar bytes. Entonces puede llamar a "name.reserve (25);" en su constructor y es probable que no se reasigne si asigna algo más pequeño. (He realizado pruebas, y parece que GCC siempre reasigna si asigna desde otra std :: cadena, pero no si asigna desde una cadena de caracteres. They say tienen una cadena de copia sobre escritura, lo que explicaría ese comportamiento)

La cadena que cree en la función unleashMonkeyFish liberará automáticamente sus recursos asignados. Esa es la característica clave de esos objetos: ellos manejan sus propias cosas. Las clases tienen un destructor que usan para liberar recursos asignados una vez que mueren los objetos, std :: string también. En mi opinión, no debes preocuparte por tener esa std :: string local en la función. De todos modos, no hará nada notable para tu rendimiento. Algunas implementaciones de std :: string (msvC++ afaik) tienen una optimización de búfer pequeño: hasta un límite pequeño, mantienen los caracteres en un búfer incrustado en lugar de asignar desde el montón.

Editar:

Como resultado, hay una mejor manera de hacer esto para las clases que tienen un eficiente swap aplicación (constante de tiempo):

void setName(std::string parameter_name) { 
    name.swap(parameter_name); 
} 

La razón de que esto es mejor, es que ahora la persona que llama sabe que el argumento se está copiando. La optimización del valor de retorno y las optimizaciones similares ahora pueden ser aplicadas fácilmente por el compilador. Considere este caso, por ejemplo

obj.setName("Mr. " + things.getName()); 

si tuviera el setName tener una referencia, a continuación, el temporal creado en el argumento sería atado a esa referencia, y dentro de setName que sería copiado, y después de que vuelve, el temporal sería destruido, que era un producto desechable de todos modos. Esto es solo subóptimo, porque el temporal en sí podría haber sido utilizado, en lugar de su copia. Tener el parámetro no como referencia hará que la persona que llama vea que el argumento se está copiando de todos modos, y hará que el trabajo del optimizador sea mucho más fácil, porque no tendría que alinear la llamada para ver que el argumento se copie de todos modos.

Para una explicación más detallada, lea el excelente artículo BoostCon09/Rvalue-References

1

Puede hacer que la cadena en unleashMonkeyFish estética, pero no creo que realmente ayude a nada (y podría ser bastante mala dependiendo de cómo se implemente).

He movido "hacia abajo" desde idiomas de nivel superior (como C#, Java) y he llegado a este mismo problema recientemente. Supongo que a menudo la única opción es copiar la cadena.

1

Si usa una variable temporal para asignar el nombre (como en su código de muestra) eventualmente tendrá que copiar la cadena a su objeto MonkeyFish para evitar que el objeto de cadena temporal vaya a fin de alcance.

Como mencionó Andrew Flanagan, puede evitar la copia de cadena utilizando una variable estática local o una constante.

Suponiendo que no sea una opción, puede al menos minimizar el número de copias de cadena a exactamente una. Pase la cadena como un puntero de referencia a setName(), y luego realice la copia dentro de la función setName(). De esta manera, puede estar seguro de que la copia se realiza una sola vez.

3

Solo para aclarar la terminología, ha creado MonkeyFish del montón (usando nuevo) y el nombre local en la pila.

Ok, entonces almacenar una referencia a un objeto es perfectamente legítimo, pero obviamente debe conocer el alcance de ese objeto. Mucho más fácil pasar la cadena por referencia, luego copiar a la variable miembro de la clase. A menos que la cuerda sea muy grande, o la realización de esta operación mucho (y quiero decir mucho, mucho), entonces realmente no hay necesidad de preocuparse.

¿Puede aclarar exactamente por qué no quiere copiar la cadena?

Editar

Un enfoque alternativo es crear un conjunto de objetos MonkeyName. Cada MonkeyName almacena un puntero a una cadena. A continuación, obtenga un nuevo MonkeyName solicitando uno del grupo (establece el nombre en la cadena interna *). Ahora pasa eso a la clase por referencia y realiza un intercambio de puntero recto. Por supuesto, el objeto MonkayName pasado se cambia, pero si vuelve directamente al grupo, eso no hará la diferencia. La única sobrecarga es la configuración real del nombre cuando obtiene MonkeyName del grupo.

... esperanza de que tenía cierto sentido :)

+0

Gracias por la corrección, actualizando la publicación. – Runcible

+0

En cuanto a por qué no quiero copiar la cadena, es exactamente como dijiste: voy a estar haciendo esto mucho, mucho. El mundo no tiene suficiente MonkeyFish. – Runcible

5

Si utiliza la siguiente declaración de método:

void setName(const std::string & parameter_name); 

entonces también sería utilizar la declaración de miembro:

std::string name; 

y la asignación en el cuerpo setName:

name = parameter_name; 

No puede declarar al miembro name como referencia porque debe inicializar un miembro de referencia en el constructor del objeto (lo que significa que no puede establecerlo en setName).

Finalmente, su implementación std::string probablemente usa cadenas contadas de referencia de todos modos, por lo que no se realiza una copia de los datos reales de cadena en la asignación. Si le preocupa el rendimiento, es mejor que esté íntimamente familiarizado con la implementación de STL que está utilizando.

+0

Sí, es un buen punto acerca de la referencia. Ese es mi error Debe establecerse solo en el constructor. – Runcible

2

Este es precisamente el problema que la referencia en el conteo está destinado a resolver. Puede utilizar Boost shared_ptr <> para hacer referencia al objeto de cadena de manera que dure al menos tanto como puntero.

Personalmente, nunca confío en él, sin embargo, prefiero ser explícito sobre la asignación y la duración de todos mis objetos. la solución de litb es preferible.

+0

¿Es justo decir que la solución de litb hace que se realice una copia, pero el uso de shared_ptrs evitaría la copia? – Runcible

+0

shared_ptr te obliga a "nuevo" el nombre local dentro de unleashMonkeyFish(), ya que debe vivir en el montón y no en la pila si quieres que sobreviva al alcance de la función. En la práctica, probablemente compilarán el mismo número de copias ya que el nuevo constructor std :: string() también debe hacer una copia. – Crashworks

+0

shared_ptr necesitará nueva la cadena. argumento que es peor que solo copiar 5 bytes de caracteres que asignar una cosa de 4 bytes del montón (y luego eliminar la cadena anterior a la que apuntaba data-member-shared_ptr) :) –

2

Cuando el compilador ve ...

std::string localname = "Wanda"; 

... será (salvo la magia optimización) emiten 0x57 0x61 0x64 0x61 0x00 0x6E [Wanda con el terminador nulo] y almacenarlo en algún lugar del la estática sección de tu código. Luego invocará std :: string (const char *) y le pasará esa dirección.Como el autor del constructor no tiene forma de saber la vida útil del const char * suministrado, debe hacer una copia. En MonkeyFish :: setName (const std :: string &), el compilador verá std :: string :: operator = (const std :: string &) y, si su std :: string se implementa con copy-on- escriba semántica, el compilador emitirá código para incrementar el recuento de referencias pero no realizará ninguna copia.

Pagará por una copia. ¿Necesitas siquiera uno? ¿Sabe en tiempo de compilación cuáles serán los nombres de MonkeyFish? ¿MonkeyFish alguna vez cambia sus nombres a algo que no se conoce en tiempo de compilación? Si se conocen todos los nombres posibles de MonkeyFish en tiempo de compilación, puede evitar toda la copia utilizando una tabla estática de literales de cadena e implementando el miembro de datos de MonkeyFish como const char *.

+0

Gracias por los comentarios tlholaday. He agregado algunos comentarios de aclaración, pero para responder su pregunta específicamente: no sé cuántos MonkeyFish habrá, y no sé sus nombres en el momento de la compilación. – Runcible

2

Como regla general, almacene sus datos como una copia dentro de una clase, y pase y devuelva datos por (const) referencia, use punteros de recuento de referencia siempre que sea posible.

No estoy tan preocupado por copiar unos 1000 bytes de datos de cadena, hasta el momento en que el generador de perfiles dice que es un costo significativo. OTOH Me importa que las estructuras de datos que contienen varios 10s de MB de datos no se copien.

2

En su código de ejemplo, sí, está obligado a copiar la cadena al menos una vez. La solución más limpia es la definición de su objeto como éste:

class MonkeyFish { 
public: 
    void setName(const std::string & parameter_name) { name = parameter_name; } 

private: 
    std::string name; 
}; 

Esto pasará una referencia a la cadena local, que se copia en una cadena permanente dentro del objeto. Cualquier solución que implique copia cero es extremadamente frágil, ya que debe tener cuidado de que la cadena que pase permanezca activa hasta que se elimine el objeto. Mejor no ir allí a menos que sea absolutamente necesario, y las copias de cadenas no son TAN costosas, preocúpate solo cuando sea necesario. :-)

Cuestiones relacionadas