2011-01-16 15 views
49

Me he dado cuenta de que suelo usar referencias constantes como valores o argumentos de retorno. Creo que la razón es que funciona casi igual que el uso de no referencia en el código. Pero definitivamente requiere más espacio y las declaraciones de funciones se alargan. Estoy de acuerdo con dicho código, pero creo que a algunas personas les parece un mal estilo de programación.int vs const int &

¿Qué opinas? ¿Vale la pena escribir const int & sobre int? Creo que está optimizado por el compilador de todos modos, así que tal vez estoy perdiendo el tiempo codificándolo, ¿no?

+1

¿cómo podría ser optimizado por el compilador? Si la función está enlineada, podría ser el compilador, pero para el caso general en el que no lo esté debe devolver exactamente lo que usted dijo que sería (excepto en algunas excepciones), ya que podría llamarlo desde una unidad de traducción diferente utilizando el prototipo especificado, por lo que debe invocarse con int/const int & (en el nivel de ensamblaje, pasar un int a algo esperando int y no es una buena idea). Combine eso con el hecho de que const int & es probable que sea más lento que int (debido a la adición de deref y la copia barata) y ahora debería saber qué hacer. – Grizzly

+1

Para el optimizador, 'const T &' es simplemente 'T &', en otras palabras, solo un puntero constante a un objeto T posiblemente cambiante. La palabra 'const' solo es vista por los programadores. – 6502

+4

@ 6502: Bueno, y por el front-end del compilador, que interviene si el programador no presta atención al 'const'. – sbi

Respuesta

122

En C++ es muy común lo que considero un antipatrón que usa const T& como una forma inteligente de decir T cuando se trata de parámetros. Sin embargo, un valor y una referencia (no importa si const o no) son dos cosas completamente diferentes y siempre y ciegamente el uso de referencias en lugar de valores puede dar lugar a errores sutiles.

La razón es que cuando se trata de referencias debe tener en cuenta dos cuestiones que no se presentan con valores: vida y aliasing.

Sólo como ejemplo un lugar donde se aplica esta anti-patrón es la biblioteca estándar sí mismo, donde std::vector<T>::push_back acepta como parámetro un const T& en lugar de un valor y esto puede reprimir por ejemplo, en código como:

std::vector<T> v; 
... 
if (v.size()) 
    v.push_back(v[0]); // Add first element also as last element 

Este código es una bomba de tiempo porque std::vector::push_back quiere una referencia constante, pero hacer push_back puede requerir una reasignación y si eso ocurre significa que después de la reasignación la referencia recibida ya no sería válida (de por vida problema) y usted ingresa el Comportamiento de comportamiento no definido.

Los problemas de alias son también una fuente de problemas sutiles si se utilizan referencias de referencias en lugar de valores.He sido mordido por ejemplo, código de este tipo:

struct P2d 
{ 
    double x, y; 
    P2d(double x, double y) : x(x), y(y) {} 
    P2d& operator+=(const P2d& p) { x+=p.x; y+=p.y; return *this; } 
    P2d& operator-=(const P2d& p) { x-=p.x; y-=p.y; return *this; } 
}; 

struct Rect 
{ 
    P2d tl, br; 
    Rect(const P2d& tl, const P2d& br) : tl(tl), bt(br) {} 
    Rect& operator+=(const P2d& p) { tl+=p; br+=p; return *this; } 
    Rect& operator-=(const P2d& p) { tl-=p; br-=p; return *this; } 
}; 

El código parece a primera vista bastante seguro, P2d es un punto bidimensional, Rect es un rectángulo y suma/resta un punto significa traducir el rectángulo .

Sin embargo, para volver a traducir el rectángulo en el origen escriba myrect -= myrect.tl; el código no funcionará porque el operador de traducción se ha definido aceptando una referencia que (en ese caso) hace referencia a un miembro de la misma instancia.

Esto significa que después de actualizar el superior izquierda con la tl -= p; superior izquierda será (0, 0) como debería, sino también p se convertirá, al mismo tiempo, porque (0, 0)p es sólo una referencia al miembro superior izquierda y así la actualización de abajo hacia la esquina derecha no funcionará porque la traducirá por (0, 0), lo que hace básicamente nada.

No se deje engañar por pensar que una referencia constante es como un valor debido a la palabra const. Esa palabra existe solo para darle errores de compilación si intenta cambiar el objeto referenciado usando esa referencia, pero no significa que el objeto al que se hace referencia es constante. Más específicamente, el objeto al que hace referencia una referencia constante puede cambiar (por ejemplo, debido a aliasing) e incluso puede desaparecer mientras lo está utilizando (de por vida problema).

En const T& la palabra const expresa una propiedad de la referencia, no de la referencia objeto: es la propiedad que hace imposible su uso para cambiar el objeto. Probablemente readonly habría sido un nombre mejor como const tiene IMO el efecto psicológico de impulsar la idea de que el objeto va a ser constante mientras usa la referencia.

Por supuesto, puede obtener aceleraciones impresionantes mediante el uso de referencias en lugar de copiar los valores, especialmente para las clases grandes. Pero siempre debe pensar en los alias y los problemas de duración cuando use referencias, ya que debajo de la tapa son solo indicadores de otros datos. Para referencias de tipos de datos "nativos" (ints, dobles, punteros), sin embargo, en realidad van a ser más lentos que los valores y no hay nada que ganar al usarlos en lugar de valores.

También una referencia constante siempre significa problemas para el optimizador como el compilador se ve obligado a ser paranoico y cada vez que se ejecuta ningún código desconocido que hay que suponer que todos los objetos referenciados pueden tener ahora un valor diferente (const por unos medios de referencia absolutamente NADA para el optimizador; esa palabra está allí solo para ayudar a los programadores; personalmente no estoy tan seguro de que sea una gran ayuda, pero esa es otra historia).

+9

+1 puntos extremadamente importantes, creo que esta es la respuesta más importante a la pregunta. – Philipp

+13

¡Gran respuesta, nunca pensé que 'push_back()' pudiera ser tan siniestro! –

+4

"[const] existe solo para darle errores de compilación si intenta cambiar el objeto al que se hace referencia utilizando esa referencia, pero no significa que el objeto al que se hace referencia es constante" No podría haberlo dicho mejor. – Dan

8

int & y int no son intercambiables! En particular, si devuelve una referencia a una variable de pila local, el comportamiento no está definido, por ejemplo:

int &func() 
{ 
    int x = 42; 
    return x; 
} 

Usted puede devolver una referencia a algo que no será destruido al final de la función (por ejemplo, un miembro estático o un miembro de la clase). Así que esto es válido:

int &func() 
{ 
    static int x = 42; 
    return x; 
} 

y con el mundo exterior, tiene el mismo efecto que devolver el int directamente (excepto que ahora se puede modificar, lo cual es por eso que ver const int & mucho).

La ventaja de la referencia es que no se requiere copia, lo cual es importante si se trata de objetos de clase grandes. Sin embargo, en muchos casos, el compilador puede optimizar eso; ver p. http://en.wikipedia.org/wiki/Return_value_optimization.

+3

Sí, sé la diferencia entre esos dos. Mi pregunta era si realmente vale la pena usar * const int & * as * int * debería optimizarse de todos modos. – Pijusn

+2

@ Valdo: en el caso de 'int', no hay ninguna ventaja en ninguna plataforma. –

13

Como dice Oli, devolver const T& en comparación con T son cosas completamente diferentes, y pueden romperse en ciertas situaciones (como en su ejemplo).

Tomando const T& en contraposición a simple T como un argumento es menos probable que rompa cosas, pero todavía tienen varias diferencias importantes.

  • Tomando T en lugar de const T& requiere que T es copiar-construible.
  • Tomando T invocará el constructor de copia, que puede ser costoso (y también el destructor al salir de la función).
  • Tomar T le permite modificar el parámetro como una variable local (puede ser más rápido que copiar manualmente).
  • Tomar const T& podría ser más lento debido a los temporales desalineados y al costo de la indirección.
+8

Además, cuando 'T' es un tipo pequeño, como' int', la copia podría ser * más barata * que la referencia. –

+2

@Peter: ¿Podría ampliar el último punto? (temporarios desalineados) –

+0

Tomando un const &, verás directamente las modificaciones hechas en un alias. – AProgrammer

3

En lugar de "pensar" que está optimizado por el compilador, ¿por qué no obtener la lista de ensamblador y averiguarlo con certeza?

junk.C++:

int my_int() 
{ 
    static int v = 5; 
    return v; 
} 

const int& my_int_ref() 
{ 
    static int v = 5; 
    return v; 
} 

La generación eléctrica ensamblador (elidido):

_Z6my_intv: 
.LFB0: 
    .cfi_startproc 
    .cfi_personality 0x3,__gxx_personality_v0 
    movl $5, %eax 
    ret 
    .cfi_endproc 

...

_Z10my_int_refv: 
.LFB1: 
    .cfi_startproc 
    .cfi_personality 0x3,__gxx_personality_v0 
    movl $_ZZ10my_int_refvE1v, %eax 
    ret 

Los movl instrucciones de ambos son muy diferentes. La primera mueve 5 a EAX (que pasa a ser el registro utilizado tradicionalmente para devolver valores en código x86 C) y la segunda mueve la dirección de una variable (detalles elididos para mayor claridad) al EAX. Esto significa que la función de llamada en el primer caso puede usar operaciones de registro directamente sin tocar la memoria para usar la respuesta, mientras que en el segundo debe ingresar a la memoria a través del puntero devuelto.

Parece que no está optimizado.

Esto está por encima de las otras respuestas le han dado aquí explicar por qué T y const T& no son intercambiables.

7

Si el destinatario y la persona que llama se definen en unidades de compilación separadas, entonces el compilador no puede optimizar la referencia. Por ejemplo, he realizado el siguiente código:

#include <ctime> 
#include <iostream> 

int test1(int i); 
int test2(const int& i); 

int main() { 
    int i = std::time(0); 
    int j = test1(i); 
    int k = test2(i); 
    std::cout << j + k << std::endl; 
} 

con G ++ en Linux de 64 bits a nivel de optimización 3. La primera llamada no necesita acceso a la memoria principal:

call time 
movl %eax, %edi  #1 
movl %eax, 12(%rsp) #2 
call _Z5test1i 
leaq 12(%rsp), %rdi #3 
movl %eax, %ebx 
call _Z5test2RKi 

línea # 1 utiliza directamente el valor de retorno en eax como argumento para test1 en edi. Las líneas 2 y 3 insertan el resultado en la memoria principal y colocan la dirección en el primer argumento porque el argumento se declara como referencia a int, por lo que debe ser posible, p. tomar su dirección. Si algo puede calcularse completamente usando registros o necesita acceder a la memoria principal puede hacer una gran diferencia en estos días. Entonces, además de ser más de tipo, const int& también puede ser más lento. La regla de oro es, pasar todos los datos que son a lo sumo tan grandes como el tamaño de palabra por valor, y todo lo demás por referencia a const. También pase los argumentos de plantilla por referencia a const; dado que el compilador tiene acceso a la definición de la plantilla, siempre puede optimizar la referencia.

+0

pasar por valor a menudo también aumenta la localidad de caché, por lo que finalmente menos líneas de caché tiene que ser cargado en el caché. – smerlin