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

Respuestas:

203

Antes se recomienda generalmente las mejores prácticas 1 a pase utilización por ref const de todo tipo , a excepción de orden interna (tipos char, int, double, etc.), para iteradores y para los objetos de función (lambdas, clases derivadas de std::*_function).

Esto era especialmente cierto antes de la existencia de la 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, un objeto se pueda pasar "por valor" sin copiarlo. En particular, este es el caso cuando el objeto que está pasando es un valor r .

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, es decir, tomará posesión 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.

"Pasar por valor" todavía hace que el objeto se copie, a menos que el objeto sea un valor. En el caso de un valor r, el objeto se puede mover en su lugar, de modo que el segundo caso ya no sea "copiar, luego mover" sino "mover, luego (potencialmente) mover de nuevo".

Para los objetos grandes que implementan constructores de movimiento adecuados (como vectores, cadenas ...), el segundo caso es mucho 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 poder darse cuenta cuando pasar por valor es costoso, y convertir implícitamente la llamada para usar una constante const si es posible.

En teoria. 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 Por ejemplo, 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.

Konrad Rudolph
fuente
hmmm ... no estoy seguro de que valga la pena pasar por ref. double-s
sergtk
3
Como de costumbre, el impulso ayuda aquí. boost.org/doc/libs/1_37_0/libs/utility/call_traits.htm tiene elementos de plantilla para descubrir automáticamente cuándo un tipo es un tipo incorporado (útil para plantillas, donde a veces no puede saberlo fácilmente).
CesarB
13
Esta respuesta pierde un punto importante. Para evitar el corte, debe pasar por referencia (constante o no). Ver stackoverflow.com/questions/274626/…
ChrisN
66
@ Chris: correcto. Dejé fuera toda la parte del polimorfismo porque esa es una semántica completamente diferente. Creo que el OP (semánticamente) significa pasar el argumento "por valor". Cuando se requieren otras semánticas, la pregunta ni siquiera se plantea.
Konrad Rudolph el
98

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

¿Quieres velocidad? Pasar 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 &), el compilador no podría haber asumido eso. dado que gi 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 gi 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ómela 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 poner 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 lo más transparente posible la persona que llama y haga esa copia temprano para que la persona que llama pueda optimizar tanto como sea 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 para 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, entonces páselo por referencia no constante

bool try_parse(T text, my::string &out) {
    /* try to parse, write result into out */
}
Johannes Schaub - litb
fuente
Encontré sus reglas buenas, pero no estoy seguro de la primera parte en la que habla sobre no pasarla, ya que un árbitro lo aceleraría. Sí, claro, pero no pasar algo como referencia solo porque la optimización no tiene ningún sentido. si desea cambiar el objeto de pila que está pasando, hágalo con ref. si no lo hace, páselo por valor. si no quieres cambiarlo, pásalo como const-ref. la optimización que viene con el paso por valor no debería importar ya que ganas otras cosas al pasar como referencia. no entiendo el "¿quieres velocidad?" SICE si va a realizar en los op le paso por valor de todos modos ..
Chikuba
Johannes: Me encantó ese artículo cuando lo leí, pero me decepcioné cuando lo probé. Este código falló tanto en GCC como en MSVC. ¿Me perdí algo o no funciona en la práctica?
user541686
No creo estar de acuerdo en que si de todos modos desea hacer una copia, la pasaría por valor (en lugar de const ref), luego muévala. Míralo de esta manera, ¿qué es más eficiente, una copia y un movimiento (incluso puedes tener 2 copias si lo pasas adelante), o solo una copia? Sí, hay algunos casos especiales a cada lado, pero si sus datos no se pueden mover de todos modos (por ejemplo, un POD con toneladas de enteros), no es necesario realizar copias adicionales.
Ion Todirel
2
Mehrdad, no estoy seguro de lo que esperaba, pero el código funciona como se esperaba
Ion Todirel
Consideraría la necesidad de copiar solo para convencer al compilador de que los tipos no se superponen a una deficiencia en el lenguaje. Prefiero usar GCC __restrict__(que también puede funcionar en referencias) que hacer copias excesivas. Lástima que C ++ no haya adoptado la restrictpalabra clave de C99 .
Ruslan
12

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 utilizan el copiador predeterminado, probablemente sería más rápido pasar por valor.

Lou Franco
fuente
Para los tipos no nativos, puede (dependiendo de qué tan bien el compilador optimice el código) obtener un aumento de rendimiento utilizando referencias constantes en lugar de solo referencias.
DO
9

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 ha sido optimizado para ser almacenado en un registro, entonces si desea pasarlo como referencia, primero debe almacenarse en la memoria y luego un puntero a esa memoria colocado 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 están pasando. Pasar las penalizaciones por pasar algo malo por valor es mucho peor que las penalizaciones de pasar un tipo incorporado por const ref.

Torlack
fuente
Nota sobre la terminología: una estructura que contiene un millón de entradas sigue siendo un "tipo POD". Posiblemente quiere decir 'para los tipos incorporados, es mejor pasar por valor'.
Steve Jessop
6

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

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

  3. Pase por un puntero no constante si el valor es costoso de copiar y la función desea 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.

Martin G
fuente
Agregue std::optionala la imagen y ya no necesita punteros.
Violet Giraffe
5

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

GeekyMonkey
fuente
No estoy seguro de por qué esto fue rechazado. Tiene sentido para mí. Si va a necesitar el valor actualmente almacenado, entonces pase por valor. Si no, pase la referencia.
Totty
44
Es totalmente dependiente del tipo. Hacer un tipo de POD (datos antiguos simples) por referencia puede, de hecho, reducir el rendimiento al causar más accesos a la memoria.
Torlack el
1
¡Obviamente, pasar int por referencia no ahorra nada! Creo que la pregunta implica cosas que son más grandes que un puntero.
GeekyMonkey
44
No es tan obvio, he visto mucho código de personas que no entienden realmente cómo funcionan las computadoras pasando cosas simples por constante porque les han dicho que es lo mejor que se puede hacer.
Torlack el
4

Como regla, 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.

sergtk
fuente
Si necesita modificar la copia del parámetro del destinatario, puede hacer una copia en el código llamado en lugar de pasar por valor. En mi opinión, generalmente no debe elegir la API en función de un detalle de implementación como ese: la fuente del código de llamada es la misma de cualquier manera, pero su código de objeto no lo es.
Steve Jessop
Si pasa por valor se crea una copia. Y en mi opinión, no importa de qué manera cree una copia: a través del argumento que pasa por valor o localmente, esto es lo que concierne a C ++. Pero desde el punto de vista del diseño, estoy de acuerdo con usted. Pero describo las características de C ++ solo aquí y no toco el diseño.
sergtk el
1

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.

Peter
fuente
Tengo entendido que en std::vectorrealidad asigna sus elementos en el montón y el objeto vector en sí nunca crece. Oh espera. Sin embargo, si la operación hace que se haga una copia del vector, de hecho irá y duplicará todos los elementos. Eso seria malo.
Steven Lu
1
Sí, eso es lo que estaba pensando. sizeof(std::vector<int>)es constante, pero pasarlo por valor seguirá copiando el contenido en ausencia de inteligencia del compilador.
Peter
1

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 clase Person. Si pasa por referencia constante, tendrá que hacer una copia para ponerlo en name_.

Germán Diago
fuente
-5

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 el parámetro de entrada y salida es diferente, es mejor usar la llamada por valor.

ejemplo void amount(int account , int deposit , int total )

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

entrada y salida es llamada de uso diferente por vaule

  1. void amount(int total , int deposit )

entrada total depósito salida total

Dhirendra Sengar
fuente