Pregunta

¿Es mejor en C ++ pasar por valor o pasar por referencia constante?

Me pregunto cuál es la mejor práctica. Me doy cuenta de que pasar por referencia constante debería proporcionar un mejor rendimiento en el programa porque no está haciendo una copia de la variable.

¿Fue útil?

Solución

Solía ??ser una práctica recomendada generalmente 1 para usar pass by const ref para todos los tipos , excepto los tipos incorporados ( char , int , double , etc.), para iteradores y para objetos de función (lambdas, clases derivadas de std :: * _ function ).

Esto era especialmente cierto antes de la existencia de semántica de movimiento . La razón es simple: si pasa por valor, debe hacerse una copia del objeto y, a excepción de los objetos muy pequeños, esto siempre es más costoso que pasar una referencia.

Con C ++ 11, hemos ganado semántica de movimiento . En pocas palabras, la semántica de movimiento permite que, en algunos casos, se pueda pasar un objeto & # 8220; por valor & # 8221; sin copiarlo En particular, este es el caso cuando el objeto que está pasando es un rvalue .

En sí mismo, mover un objeto sigue siendo al menos tan costoso como pasar por referencia. Sin embargo, en muchos casos, una función copiará internamente un objeto de todos modos & # 8212; es decir, tomará propiedad del argumento. 2

En estas situaciones tenemos la siguiente compensación (simplificada):

  1. Podemos pasar el objeto por referencia y luego copiarlo internamente.
  2. Podemos pasar el objeto por valor.

& # 8220; Pase por valor & # 8221; aún hace que el objeto se copie, a menos que el objeto sea un valor r. En el caso de un valor r, el objeto se puede mover en su lugar, de modo que el segundo caso de repente ya no & # 8220; copiar, luego mover & # 8221; pero & # 8220; mover, luego (potencialmente) mover de nuevo & # 8221 ;.

Para objetos grandes que implementan constructores de movimiento adecuados (como vectores, cadenas & # 8230;), el segundo caso es entonces enormemente más eficiente que el primero. Por lo tanto, se recomienda usar pasar por valor si la función toma posesión del argumento y si el tipo de objeto admite un movimiento eficiente .


Una nota histórica:

De hecho, cualquier compilador moderno debería ser capaz de determinar cuándo pasar por valor es costoso, y convertir implícitamente la llamada para usar una constante const si es posible.

En teoría. En la práctica, los compiladores no siempre pueden cambiar esto sin romper la interfaz binaria de la función. En algunos casos especiales (cuando la función está en línea), la copia se eliminará si el compilador puede descubrir que el objeto original no se cambiará a través de las acciones en la función.

Pero, en general, el compilador no puede determinar esto, y el advenimiento de la semántica de movimiento en C ++ ha hecho que esta optimización sea mucho menos relevante.


1 Ej. en Scott Meyers, C ++ efectivo .

2 Esto es especialmente cierto para los constructores de objetos, que pueden tomar argumentos y almacenarlos internamente para formar parte del estado del objeto construido.

Otros consejos

Editar: Nuevo artículo de Dave Abrahams en cpp-next:

¿Quieres velocidad? Pase por valor.


Pasar por valor para estructuras donde la copia es barata tiene la ventaja adicional de que el compilador puede asumir que los objetos no tienen alias (no son los mismos objetos). Usando paso por referencia, el compilador no puede asumir eso siempre. Ejemplo simple:

foo * f;

void bar(foo g) {
    g.i = 10;
    f->i = 2;
    g.i += 5;
}

el compilador puede optimizarlo en

g.i = 15;
f->i = 2;

ya que sabe que f y g no comparten la misma ubicación. Si g fuera una referencia (foo & amp;), el compilador no podría haber asumido eso. dado que g.i podría ser alias por f- > i y tener que tener un valor de 7. por lo que el compilador tendría que recuperar el nuevo valor de g.i de la memoria.

Para reglas más prácticas, aquí hay un buen conjunto de reglas que se encuentran en el artículo Move Constructors ( lectura muy recomendable).

  • Si la función tiene la intención de cambiar el argumento como un efecto secundario, tómalo como referencia no constante.
  • Si la función no modifica su argumento y el argumento es de tipo primitivo, tómelo por valor.
  • De lo contrario, tómalo por referencia constante, excepto en los siguientes casos
    • Si la función necesitaría hacer una copia de la referencia constante de todos modos, tómala por valor.

" Primitivo " arriba significa básicamente tipos de datos pequeños que tienen unos pocos bytes de longitud y no son polimórficos (iteradores, objetos de función, etc.) ni caros de copiar. En ese documento, hay otra regla. La idea es que a veces uno quiere hacer una copia (en caso de que el argumento no se pueda modificar), y a veces no quiere (en caso de que quiera usar el argumento en sí mismo en la función si el argumento era temporal de todos modos , por ejemplo). El documento explica en detalle cómo se puede hacer eso. En C ++ 1x, esa técnica se puede usar de forma nativa con soporte de lenguaje. Hasta entonces, iría con las reglas anteriores.

Ejemplos: para hacer una cadena en mayúscula y devolver la versión en mayúscula, siempre se debe pasar por valor: de todos modos, se debe tomar una copia (no se puede cambiar la referencia constante directamente), así que mejor hacerlo transparente lo más posible para la persona que llama y haga esa copia lo antes posible para que la persona que llama pueda optimizar lo más posible, como se detalla en ese documento:

my::string uppercase(my::string s) { /* change s and return it */ }

Sin embargo, si no necesita cambiar el parámetro de todos modos, tómelo como referencia const:

bool all_uppercase(my::string const& s) { 
    /* check to see whether any character is uppercase */
}

Sin embargo, si el propósito del parámetro es escribir algo en el argumento, páselo por referencia no constante

bool try_parse(T text, my::string &out) {
    /* try to parse, write result into out */
}

Depende del tipo. Está agregando la pequeña sobrecarga de tener que hacer una referencia y desreferenciar. Para los tipos con un tamaño igual o menor que los punteros que usan el copiador predeterminado, probablemente sería más rápido pasar por valor.

Como se ha señalado, depende del tipo. Para los tipos de datos integrados, es mejor pasar por valor. Incluso algunas estructuras muy pequeñas, como un par de entradas, pueden funcionar mejor al pasar por valor.

Aquí hay un ejemplo, suponga que tiene un valor entero y desea pasarlo a otra rutina. Si ese valor se ha optimizado para almacenarse en un registro, entonces si desea pasarlo como referencia, primero debe almacenarse en la memoria y luego se debe colocar un puntero a esa memoria en la pila para realizar la llamada. Si se pasaba por valor, todo lo que se requiere es el registro empujado a la pila. (Los detalles son un poco más complicados que los dados con diferentes sistemas de llamadas y CPU).

Si está haciendo programación de plantillas, por lo general se ve obligado a pasar siempre por const ref ya que no conoce los tipos que se pasan. Pasar las penalizaciones por pasar algo malo por valor es mucho peor que las penalizaciones de pasar un construido -in type por const ref.

Parece que obtuviste tu respuesta. Pasar por valor es costoso, pero le da una copia para trabajar si lo necesita.

Esto es lo que normalmente trabajo cuando diseño la interfaz de una función que no es de plantilla:

  1. Pase por valor si la función no quiere modificar el parámetro y el el valor es barato de copiar (int, double, float, char, bool, etc ... Observe que std :: string, std :: vector, y el resto de los contenedores en la biblioteca estándar NO lo son)

  2. Pase por puntero constante si el valor es costoso de copiar y la función sí no desea modificar el valor señalado y NULL es un valor que maneja la función.

  3. Pase por puntero no constante si el valor es costoso de copiar y la función quiere modificar el valor señalado y NULL es un valor que maneja la función.

  4. Pase por referencia constante cuando el valor es costoso de copiar y la función no quiere modificar el valor mencionado y NULL no sería un valor válido si se utilizara un puntero.

  5. Pase por referencia no constante cuando el valor es costoso de copiar y la función quiere modificar el valor mencionado y NULL no sería un valor válido si se utilizara un puntero.

Como regla general, pasar por referencia constante es mejor. Pero si necesita modificar el argumento de su función localmente, debería usar el paso por valor. Para algunos tipos básicos, el rendimiento en general es el mismo tanto para pasar por valor como por referencia. En realidad, la referencia interna representada por el puntero, es por eso que puede esperar, por ejemplo, que para el puntero ambos pases sean iguales en términos de rendimiento, o incluso pasar por valor puede ser más rápido debido a una desreferencia innecesaria.

Como regla general, valor para tipos que no son de clase y referencia constante para clases. Si una clase es realmente pequeña, probablemente sea mejor pasar por valor, pero la diferencia es mínima. Lo que realmente desea evitar es pasar una clase gigantesca por valor y tenerlo todo duplicado; esto hará una gran diferencia si está pasando, por ejemplo, un std :: vector con bastantes elementos en él.

Pase por valor para tipos pequeños.

Pase por referencias constantes para tipos grandes (la definición de grande puede variar entre máquinas) PERO, en C ++ 11, pase por valor si va a consumir los datos, ya que puede explotar la semántica de movimiento. Por ejemplo:

class Person {
 public:
  Person(std::string name) : name_(std::move(name)) {}
 private:
  std::string name_;
};

Ahora el código de llamada haría:

Person p(std::string("Albert"));

Y solo se crearía un objeto y se movería directamente al miembro name_ en la clase Person . Si pasa por referencia constante, tendrá que hacer una copia para ponerla en name_ .

Diferencia simple: - En la función tenemos parámetros de entrada y salida, por lo que si su parámetro de entrada y salida de paso es el mismo, use la llamada por referencia; de lo contrario, si los parámetros de entrada y salida son diferentes, es mejor usar la llamada por valor.

ejemplo cantidad nula (int cuenta, int depósito, int total)

parámetro de entrada: cuenta, depósito parámetro de salida: total

entrada y salida es llamada de uso diferente por vaule

  1. cantidad nula (int total, int deposit)

ingreso depósito total total de salida

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