2012-01-06 14 views
22

Según "How to get around the warning "rvalue used as lvalue"?", Visual Studio simplemente advertir sobre el código como este: no¿Por qué es ilegal tomar temporalmente la dirección de un rvalue?

int bar() { 
    return 3; 
} 

void foo(int* ptr) { 

} 

int main() { 
    foo(&bar()); 
} 

En C++ Es permitió a tomar la dirección de un temporal (o, al menos, de un objeto a la que se hace referencia en rvalue expresión?), y pensé que esto se debía a que no se garantiza que los temporales ni siquiera tengan almacenamiento.

Pero entonces, a pesar de los diagnósticos se pueden presentar en cualquier forma el compilador elige, yo todavía he esperados MSVS a de error en lugar de advierten en tal caso.

Por lo tanto, ¿tiene garantía de almacenamiento provisional? Y si es así, ¿por qué el código anterior no se permite en primer lugar?

+0

** relacionados: ** http://stackoverflow.com/questions/4301179/why-is-taking-the-address-of-a-temporary-illegal (aunque no estoy totalmente convencido de que es la misma) –

+0

una de las respuestas más épicas en SO se aplica muy bien a su pregunta: [¿Se puede acceder a la memoria de una variable local fuera de su alcance?] (Http://stackoverflow.com/a/6445794/1025391) – moooeeeep

+5

MSVS puede haga las extensiones de lenguaje que desee. Estoy de acuerdo contigo en que es extraño, sin embargo. –

Respuesta

5

Tienes razón al decir que "los temporales no se garantiza que incluso tienen el almacenamiento", en el sentido de que el temporal no se puede almacenar en la memoria direccionable. De hecho, muy a menudo las funciones compiladas para arquitecturas RISC (por ejemplo, ARM) devolverán valores en registros de uso general y también esperarían entradas en esos registros.

MSVS, produciendo el código para las arquitecturas x86, puede siempre producen funciones que devuelven sus valores en la pila. Por lo tanto, están almacenados en memoria direccionable y tienen una dirección válida.

9

Ciertamente, los temporales tienen capacidad de almacenamiento. Se podría hacer algo como esto:

template<typename T> 
const T *get_temporary_address(const T &x) { 
    return &x; 
} 

int bar() { return 42; } 

int main() { 
    std::cout << (const void *)get_temporary_address(bar()) << std::endl; 
} 

En C++ 11, se puede hacer esto con referencias rvalue no const también:

template<typename T> 
T *get_temporary_address(T &&x) { 
    return &x; 
} 

int bar() { return 42; } 

int main() { 
    std::cout << (const void *)get_temporary_address(bar()) << std::endl; 
} 

Tenga en cuenta, por supuesto, que la eliminación de referencias del puntero en cuestión (fuera de get_temporary_address sí mismo) es una muy mala idea; lo temporal solo vive hasta el final de la expresión completa, y por lo tanto tener un puntero escapando a la expresión es casi siempre una receta para el desastre.

Además, tenga en cuenta que nunca se requiere compilador para rechazar un programa no válido. Los estándares de C y C++ se limitan a poner para el diagnóstico (es decir, un error o aviso), en la que el compilador puede rechazar el programa, o se pueden compilar un programa, con un comportamiento indefinido en tiempo de ejecución. Si desea que su compilador rechace estrictamente los programas que producen diagnósticos, configúrelo para convertir advertencias en errores.

+2

Tenga en cuenta que está muy cerca de un comportamiento indefinido. El temporal introducido por 'bar()' solo durará hasta el final de la expresión completa, que en este caso podría no ser del todo clara: 'std :: cout.operator << ((const void *) get_temporary_address (bar())) '. – Xeo

+0

@ Xeo, de hecho. Sin embargo, convertir el puntero a un entero sin desreferenciarlo debería estar bien definido, ¿no es así? El valor exacto no estaría especificado, por supuesto. – bdonlan

+0

Sé que no se requiere un compilador, pero esperaría que uno convencional actuara con sensatez. Supongo que esta pregunta se trata tanto de adivinar el alcance de la sensibilidad de VS como de si los temporales tienen almacenamiento. –

4

Los objetos temporales tienen memoria. Algunas veces el compilador crea temporarios también. En casos potosos, estos objetos están a punto de desaparecer, es decir, no deberían recopilar cambios importantes por casualidad. Por lo tanto, puede obtener un temporal solo a través de una referencia rvalue o una referencia constante, pero no a través de una referencia no const. Tomar la dirección de un objeto que está a punto de desaparecer también se siente como algo peligroso y, por lo tanto, no es compatible.

Si está seguro de que realmente desea una referencia no const o un puntero de un objeto temporal, puede devolverlo desde una función de miembro correspondiente: puede llamar a funciones de miembros no const en temporales. Y puede devolver this desde este miembro. Sin embargo, tenga en cuenta que el sistema de tipo está tratando de ayudarlo. Cuando lo engañas, es mejor que sepas que lo que estás diciendo es lo correcto.

+0

No quiero _use_ esto; Solo trato de racionalizar sobre este comportamiento diagnóstico. Ya indiqué en la pregunta que no puede tomar la dirección de un temporal :) –

1

Las temporarias tienen almacenamiento. Se asignan en la pila de la persona que llama (nota: podrían ser objeto de convención de llamada, pero creo que la pila todo el uso de la persona que llama):

caller() 
{ 
callee1(Tmp()); 
callee2(Tmp()); 
} 

compilador asignar espacio para el resultado Tmp() en la pila de la caller. Puede tomar la dirección de esta ubicación de memoria: será una dirección en la pila del caller.Lo que el compilador no garantiza es que conservará los valores en esta dirección de pila después de callee devuelve. Por ejemplo, el compilador puede colocar allí otra temporal, etc.

EDIT: Creo, que ha anulado para eliminar código como este:

T bar(); 
T * ptr = &bar(); 

, ya que es muy probable que conducen a problemas.

EDIT: aquí hay una little test: GCC

#include <iostream> 

typedef long long int T64; 

T64 ** foo(T64 * fA) 
{ 

std::cout << "Address of tmp inside callee : " << &fA << std::endl; 

return (&fA); 
} 

int main(void) 
{ 
T64 lA = -1; 
T64 lB = -2; 
T64 lC = -3; 
T64 lD = -4; 

T64 ** ptr_tmp = foo(&lA); 
std::cout << "**ptr_tmp = *(*ptr_tmp) = lA\t\t\t\t**" << ptr_tmp << " = *(" << *ptr_tmp << ") = " << **ptr_tmp << " = " << lA << std::endl << std::endl; 

foo(&lB); 
std::cout << "**ptr_tmp = *(*ptr_tmp) = lB (compiler override)\t**" << ptr_tmp << " = *(" << *ptr_tmp << ") = " << **ptr_tmp << " = " << lB << std::endl 
    << std::endl; 

*ptr_tmp = &lC; 
std::cout << "Manual override" << std::endl << "**ptr_tmp = *(*ptr_tmp) = lC (manual override)\t\t**" << ptr_tmp << " = *(" << *ptr_tmp << ") = " << **ptr_tmp 
    << " = " << lC << std::endl << std::endl; 

*ptr_tmp = &lD; 
std::cout << "Another attempt to manually override" << std::endl; 
std::cout << "**ptr_tmp = *(*ptr_tmp) = lD (manual override)\t\t**" << ptr_tmp << " = *(" << *ptr_tmp << ") = " << **ptr_tmp << " = " << lD << std::endl 
    << std::endl; 

return (0); 
} 

Programa de salida:

Address of tmp inside callee : 0xbfe172f0 
**ptr_tmp = *(*ptr_tmp) = lA    **0xbfe172f0 = *(0xbfe17328) = -1 = -1 

Address of tmp inside callee : 0xbfe172f0 
**ptr_tmp = *(*ptr_tmp) = lB (compiler override) **0xbfe172f0 = *(0xbfe17320) = -2 = -2 

Manual override 
**ptr_tmp = *(*ptr_tmp) = lC (manual override)  **0xbfe172f0 = *(0xbfe17318) = -3 = -3 

Another attempt to manually override 
**ptr_tmp = *(*ptr_tmp) = lD (manual override)  **0xbfe172f0 = *(0x804a3a0) = -5221865215862754004 = -4 

La salida del programa de VC++:

Address of tmp inside callee : 00000000001EFC10 
**ptr_tmp = *(*ptr_tmp) = lA       **00000000001EFC10 = *(000000013F42CB10) = -1 = -1 

Address of tmp inside callee : 00000000001EFC10 
**ptr_tmp = *(*ptr_tmp) = lB (compiler override)  **00000000001EFC10 = *(000000013F42CB10) = -2 = -2 

Manual override 
**ptr_tmp = *(*ptr_tmp) = lC (manual override)   **00000000001EFC10 = *(000000013F42CB10) = -3 = -3 

Another attempt to manually override 
**ptr_tmp = *(*ptr_tmp) = lD (manual override)   **00000000001EFC10 = *(000000013F42CB10) = 5356268064 = -4 

Aviso, tanto GCC y reserva VC++ en la pila de main variables locales ocultas para los temporales y MIGHT silenciosamente usalos, usalos a ellos. Todo va normal, hasta la última anulación manual: después de la última anulación manual tenemos una llamada adicional adicional al std::cout. Usa el espacio de la pila donde simplemente escribimos algo, y como resultado obtenemos basura.

Línea inferior: tanto GCC como VC++ asignan espacio para los temporales en la pila del llamador. Podrían tener diferentes estrategias sobre cuánto espacio asignar, cómo reutilizar este espacio (también podría depender de las optimizaciones). Ambos pueden reutilizar este espacio a su discreción y, por lo tanto, no es seguro tomar la dirección de un temporal, ya que podríamos tratar de acceder a través de esta dirección el valor que asumimos que todavía tiene (por ejemplo, escribir algo allí directamente y luego intentar para recuperarlo), mientras que el compilador podría haberlo reutilizado y sobrescribir nuestro valor.

3

Como han mencionado otros, todos los temporarios acordamos tener almacenamiento.

¿por qué es ilegal tomar la dirección de un temporal?

Dado que los temporales se asignan en la pila, el compilador puede usar esa dirección libremente para cualquier otro fin que desee.

int foo() 
{ 
int myvar=5; 
return &myvar; 
} 

int main() 
{ 
int *p=foo(); 
print("%d", *p); 
return 0; 
} 

Digamos que la dirección de 'myvar' es 0x1000. Es muy probable que este programa imprima 99 a pesar de que es ilegal acceder a 0x1000 en main(). Sin embargo, no necesariamente todo el tiempo.

con un ligero cambio de la principal por encima de():

int foo() 
{ 
int myvar=5; 
return &myvar; // address of myvar is 0x1000 
} 

int main() 
{ 
int *p=foo(); //illegal to access 0x1000 here 
print("%d", *p); 
fun(p); // passing *that address* to fun() 
return 0; 
} 

void fun(int *q) 
{ 
int a,b; //some variables 
print("%d", *q); 
} 

es muy poco probable que imprimir '5' como el compilador podría haber incluso asignado la misma porción de la pila (que contiene 0x1000) El segundo printf por diversión() también. No importa si imprime '5' para ambos printfs O en cualquiera de ellos, es puramente un efecto colateral involuntario sobre cómo se usa/asigna la memoria de pila. Es por eso que es ilegal acceder a una dirección que no está viva en el alcance.

+0

Claramente, dado que VS no cumple con la regla, es técnicamente posible evitar esto. –

37

En realidad, en el diseño del idioma original que era permitido tomar la dirección de un temporal. Como habrás notado correctamente, no hay una razón técnica para no permitir esto, y MSVC todavía lo permite a través de una extensión de idioma no estándar.

La razón por la C++ hizo ilegal es que las referencias a Temporaries enfrentamientos con otra característica lenguaje C++ que fue heredado de la unión C: conversión implícita. considerar:

void CalculateStuff(long& out_param) { 
    long result; 
    // [...] complicated calculations 
    out_param = result; 
} 

int stuff; 
CalculateStuff(stuff); //< this won't compile in ISO C++ 

CalculateStuff() se supone que devolver su resultado a través del parámetro de salida. Pero lo que realmente sucede es esto: la función acepta un long&, pero se le da un argumento del tipo int. A través de la conversión de tipo implícito de C, que int está ahora implícitamente convierte en una variable de tipo long, la creación de un anónimo temporal en el proceso. Por lo tanto, en lugar de la variable stuff, la función realmente funciona en un temporal sin nombre, y todos los efectos secundarios aplicados por esa función se perderán una vez que ese temporal se destruya. El valor de la variable stuff nunca cambia. Las referencias se introdujeron en C++ para permitir la sobrecarga del operador, porque desde el punto de vista del llamante son sintácticamente idénticas a las llamadas con valores por defecto Lamentablemente, es exactamente esa equivalencia sintáctica la que genera problemas cuando se combina con la conversión de tipo implícito de C.

Desde BS quería mantener ambas características (los números y C-compatibilidad), introdujo la regla que todos conocemos hoy en día: los temporales sin nombre sólo se unen a las referencias const. Con esa regla adicional, la muestra anterior ya no se compila. Dado que el problema solo ocurre cuando la función aplica efectos secundarios a un parámetro de referencia, aún es seguro vincular los temporales no nombrados a las referencias const, que por lo tanto todavía está permitido.

Toda esta historia también se describe en el capítulo 3.7 del Diseño y Evolución de C++:

La razón para permitir que las referencias sean inicializados por los no lvalues ​​fue permitir la distinción entre la llamada por valor y llamada por referencia para que sea un detalle especificado por la función llamada y que no interese a la persona que llama. Para las referencias const, esto es posible; para non-const referencias no es. Para la versión 2.0, la definición de C++ se modificó para reflejar esto.

También recuerdo vagamente leer en un documento que descubrió este comportamiento por primera vez, pero no recuerdo ahora. ¿Quizás alguien pueda ayudarme?

Cuestiones relacionadas