¿El estándar de C++ garantiza que el valor de retorno de una función tenga una dirección constante?
c++11 return-value (2)
Considere este programa:
#include <stdio.h>
struct S {
S() { print(); }
void print() { printf("%p/n", (void *) this); }
};
S f() { return {}; }
int main() { f().print(); }
Por lo que puedo decir, hay exactamente un objeto S
construido aquí. No se está produciendo ninguna elección de copia: en primer lugar, no hay ninguna copia que deba eliminarse y, de hecho, si elimino explícitamente la copia y / o el constructor de movimiento, los compiladores siguen aceptando el programa.
Sin embargo, veo dos valores de puntero diferentes impresos. Esto sucede porque la ABI de mi plataforma devuelve tipos trivialmente copiables como este en los registros de la CPU, por lo que no hay forma de que la ABI evite una copia. clang conserva este comportamiento incluso al optimizar la llamada de función por completo. Si le doy a S
un constructor de copia no trivial, aunque sea inaccesible, entonces veo el mismo valor impreso dos veces.
La llamada inicial a print()
ocurre durante la construcción, que es antes del inicio de la vida útil del objeto, pero usar this
dentro de un constructor normalmente es válido siempre y cuando no se use de una manera que requiera que la construcción haya terminado. no hay conversión a una clase derivada, por ejemplo, y por lo que sé, imprimir o almacenar su valor no requiere que la construcción haya terminado.
¿El estándar permite que este programa imprima dos valores de puntero diferentes?
Nota: soy consciente de que el estándar permite que este programa imprima dos representaciones diferentes del mismo valor de puntero y, técnicamente, no lo he descartado. Podría crear un programa diferente que evite comparar representaciones de punteros, pero sería más difícil de entender, así que me gustaría evitar eso si es posible.
Esto no es una respuesta, sino una nota sobre el comportamiento diferente de g ++ y clang en este caso, dependiendo de la -O
optimización -O
.
Considere el siguiente código:
#include <stdio.h>
struct S {
int i;
S(int _i): i(_i) {
int* p = print("from ctor");
printf("about to put 5 in %p/n", (void *)&i);
*p = 5;
}
int* print(const char* s) {
printf("%s: %p %d %p/n", s, (void *) this, i, (void *)&i);
return &i;
}
};
S f() { return {3}; }
int main() {
f().print("from main");
}
Podemos ver que clang (3.8) y g ++ (6.1) lo están tomando de manera un poco diferente, pero ambos llegan a la respuesta correcta.
clang (para no -O, -O1, -O2) y g ++ (para no -O, -O1)
from ctor: 0x7fff9d5e86b8 3 0x7fff9d5e86b8
about to put 5 in 0x7fff9d5e86b8
from main: 0x7fff9d5e86b0 5 0x7fff9d5e86b0
g ++ (para -O2)
from ctor: 0x7fff52a36010 3 0x7fff52a36010
about to put 5 in 0x7fff52a36010
from main: 0x7fff52a36010 5 0x7fff52a36010
Parece que ambos lo hacen bien en ambos casos: cuando deciden omitir la optimización del registro (g ++ -O2) y cuando van con la optimización del registro pero copian el valor a la i real a tiempo (en todos los demás casos).
TC señaló en los comentarios que esto es un defecto en la norma. Es el problema del lenguaje central 1590 . Es un problema sutilmente diferente a mi ejemplo, pero la misma causa raíz:
Algunas ABI requieren que un objeto de ciertos tipos de clase se pase en un registro [...]. El estándar debe ser cambiado para permitir este uso.
La redacción actual sugerida cubriría esto agregando una nueva regla a la norma:
Cuando un objeto de clase de clase
X
se pasa o se devuelve desde una función, si cada constructor de copia, mover constructor y destructor deX
es trivial o eliminado, yX
tiene al menos una copia o constructor de movimiento no eliminado, las implementaciones son permitido para crear un objeto temporal para contener el parámetro de función u objeto de resultado. [...]
En su mayor parte, esto permitiría el comportamiento actual de GCC / clang.
Hay un pequeño caso en la esquina: actualmente, cuando un tipo solo tiene una copia o un constructor de movimiento que sería trivial si las reglas actuales de la norma estuvieran predeterminadas, ese constructor aún es trivial si se elimina:
12.8 Copiar y mover objetos de clase [class.copy]
12 Un constructor de copiar / mover para la clase
X
es trivial si no es proporcionado por el usuario [...]
Un constructor de copia eliminado no es proporcionado por el usuario, y nada de lo que sigue haría que tal constructor de copia no sea trivial. Entonces, tal como lo especifica el estándar, dicho constructor es trivial, y como lo especifica el ABI de mi plataforma , debido a que el constructor trivial, GCC y clang también crean una copia adicional en ese caso. Una adición de una línea a mi programa de prueba demuestra esto:
#include <stdio.h>
struct S {
S() { print(); }
S(const S &) = delete;
void print() { printf("%p/n", (void *) this); }
};
S f() { return {}; }
int main() { f().print(); }
Esto imprime dos direcciones diferentes con GCC y clang, incluso aunque la resolución propuesta requiera que se imprima la misma dirección dos veces. Esto parece sugerir que, aunque obtendremos una actualización de la norma para no requerir un ABI radicalmente incompatible, todavía necesitaremos una actualización de la ABI para manejar el caso de la esquina de una manera compatible con lo que requerirá la norma.