¿Por qué fluir desde el final de una función no nula sin devolver un valor no produce un error de compilación?
Desde que me di cuenta hace muchos años de que esto no produce un error de forma predeterminada (al menos en GCC), siempre me he preguntado por qué.
Entiendo que puede emitir indicadores del compilador para generar una advertencia, pero ¿no debería ser siempre un error? ¿Por qué tiene sentido que una función no nula que no devuelve un valor sea válida?
Un ejemplo según lo solicitado en los comentarios:
#include <stdio.h>
int stringSize()
{
}
int main()
{
char cstring[5];
printf( "the last char is: %c\n", cstring[stringSize()-1] );
return 0;
}
...compila.
Los estándares C99 y C++ requieren que no sean void
funciones para devolver un valor, excepto main
. main
Se definirá la declaración de devolución que falta (para devolver 0
). En C++ es un comportamiento indefinido si la ejecución realmente llega al final de una void
función que no es distinta de main
, mientras que en C es solo UB si la persona que llama usa el valor de retorno.
Esto significa que puede parecer que las funciones podrían llegar al final sin devolver un valor, pero en realidad no pueden llegar al cierre }
. La respuesta de John Kugelman muestra algunos ejemplos, como una función sin retorno llamada desde un lado de un if
. Solo es un comportamiento indefinido si la ejecución realmente llega al final sin llegar a un return
punto anterior. El fundamento incluye que verificar si cada ruta de código real devuelve un valor es bastante difícil (sin saber qué funciones nunca regresan), por lo que no es ilegal compilar una función como su ejemplo, solo llamarla como lo main
hace el suyo.
Como extensión, al menos un compilador (MSVC) permite establecer un valor de retorno con un ensamblado en línea , pero la mayoría de los demás aún requieren una declaración de retorno en funciones que usan inline asm
.
Del borrador de C++ 11 :
§ 6.6.3/2
Salir del final de una función [...] da como resultado un comportamiento indefinido en una función de retorno de valor.
§ 3.6.1/5
Si el control llega al final de
main
sin encontrar unareturn
declaración, el efecto es el de ejecutarreturn 0;
Tenga en cuenta que el comportamiento descrito en C++ 6.6.3/2 no es el mismo en C.
gcc le dará una advertencia si lo llama con la opción -Wreturn-type.
-Wreturn-type Advierte cada vez que se define una función con un tipo de retorno cuyo valor predeterminado es int. También advierte sobre cualquier declaración de retorno sin valor de retorno en una función cuyo tipo de retorno no sea nulo (la caída del final del cuerpo de la función se considera que regresa sin un valor), y sobre una declaración de retorno con una expresión en una función cuyo el tipo de retorno es nulo.
Esta advertencia está habilitada por -Wall .
Solo como curiosidad, mira lo que hace este código:
#include <iostream>
int foo() {
int a = 5;
int b = a + 1;
}
int main() { std::cout << foo() << std::endl; } // may print 6
Este código tiene un comportamiento formalmente indefinido y, en la práctica, las llamadas dependen de la convención y la arquitectura . En un sistema en particular, con un compilador en particular, el valor de retorno es el resultado de la última evaluación de expresión, almacenada en el eax
registro del procesador de ese sistema, si deshabilita la optimización.
Esto parece ser una consecuencia de las funciones internas de GCC con la optimización deshabilitada, porque en ese caso elige el registro de valor de retorno si lo necesita para implementar una declaración. Con la optimización habilitada en modo C++, GCC y clang asumen que esta ruta de ejecución es inalcanzable porque contiene un comportamiento indefinido. Ni siquiera emiten una ret
instrucción, por lo que la ejecución recae en la siguiente función en la sección .text. Por supuesto, un comportamiento indefinido significa que podría pasar cualquier cosa.
gcc no comprueba de forma predeterminada que todas las rutas de código devuelvan un valor porque, en general, esto no se puede hacer. Se supone que sabes lo que estás haciendo. Considere un ejemplo común que utiliza enumeraciones:
Color getColor(Suit suit) {
switch (suit) {
case HEARTS: case DIAMONDS: return RED;
case SPADES: case CLUBS: return BLACK;
}
// Error, no return?
}
Usted, el programador, sabe que, salvo que haya un error, este método siempre devuelve un color. gcc confía en que usted sabe lo que está haciendo, por lo que no lo obliga a colocar una devolución al final de la función.
javac, por otro lado, intenta verificar que todas las rutas de código devuelvan un valor y arroja un error si no puede probar que todas lo hacen. Este error lo exige la especificación del lenguaje Java. Tenga en cuenta que a veces es incorrecto y debe incluir una declaración de devolución innecesaria.
char getChoice() {
int ch = read();
if (ch == -1 || ch == 'q') {
System.exit(0);
}
else {
return (char) ch;
}
// Cannot reach here, but still an error.
}
Es una diferencia filosófica. C y C++ son lenguajes más permisivos y confiables que Java o C#, por lo que algunos errores en los lenguajes más nuevos son advertencias en C/C++ y algunas advertencias se ignoran o desactivan de forma predeterminada.