¿Por qué el flujo del final de una función no nula sin devolver un valor no produce un error del compilador?

158

Desde que me di cuenta hace muchos años, que esto no produce un error por defecto (al menos en GCC), siempre me he preguntado por qué.

Entiendo que puede emitir indicadores de compilación para generar una advertencia, pero ¿no debería ser siempre un error? ¿Por qué tiene sentido que una función no nula no devuelva un valor para ser válido?

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.

Catskul
fuente
9
Alternativamente, trato todas las advertencias por triviales que sean como errores, y activo todas las advertencias que puedo (con desactivación local si es necesario ... pero luego está claro en el código por qué).
Matthieu M.
8
-Werror=return-typetratará solo esa advertencia como un error. Simplemente ignoré la advertencia y el par de minutos de frustración que encontraron un thispuntero inválido me llevaron aquí y a esta conclusión.
jozxyqk
Esto empeora por el hecho de que fluir del final de una std::optionalfunción sin regresar devuelve un "verdadero" opcional
Rufus
@Rufus No tiene que hacerlo. Eso fue exactamente lo que sucedió en su máquina / compilador / sistema operativo / ciclo lunar. Cualquier código basura que haya generado el compilador debido al comportamiento indefinido parece casi como un "verdadero" opcional, sea lo que sea.
underscore_d

Respuestas:

146

Los estándares C99 y C ++ no requieren funciones para devolver un valor. La declaración de retorno faltante en una función de retorno de valor se definirá (para devolver 0) solo en la mainfunción.

La lógica incluye que verificar si cada ruta de código devuelve un valor es bastante difícil, y un valor de retorno podría establecerse con un ensamblador incorporado u otros métodos complicados.

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 mainsin encontrar una return declaración, el efecto es el de ejecutar

return 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 Avisa siempre que una función se define con un tipo de retorno que por defecto es int. También advierta sobre cualquier declaración de retorno sin valor de retorno en una función cuyo tipo de retorno no es nulo (se considera que la devolución del extremo del cuerpo de la función devuelve sin un valor), y sobre una declaración de retorno con una expresión en una función cuyo return-type 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, llama a convenciones y depende de la arquitectura . En un sistema particular, con un compilador particular, el valor de retorno es el resultado de la última evaluación de expresión, almacenada en el eaxregistro del procesador de ese sistema.

fnieto - Fernando Nieto
fuente
13
Sería cauteloso de llamar "permitido" el comportamiento indefinido, aunque es cierto que también me equivocaría al llamarlo "prohibido". No ser un error y no requerir un diagnóstico no es lo mismo que "permitido". Por lo menos, su respuesta se lee un poco como si dijera que está bien hacerlo, que en gran medida no lo es.
ligereza corre en órbita el
44
@Catskul, ¿por qué compras ese argumento? ¿No sería factible, si no es trivialmente fácil, identificar los puntos de salida de una función y asegurarse de que todos devuelvan un valor (y un valor del tipo de retorno declarado)?
BlueBomber
3
@Catskul, sí y no. Los idiomas estáticamente escritos y / o compilados hacen muchas cosas que probablemente consideraría "prohibitivamente caras", pero debido a que solo lo hacen una vez, en el momento de la compilación, tienen un gasto insignificante. Incluso habiendo dicho eso, no veo por qué identificar los puntos de salida de una función debe ser superlineal: simplemente atraviesa el AST de la función y busca las llamadas de retorno o de salida. Eso es tiempo lineal, que es decididamente eficiente.
BlueBomber
3
@LightnessRacesinOrbit: si una función con un valor de retorno a veces regresa inmediatamente con un valor y a veces llama a otra función que siempre sale a través de throwo longjmp, ¿debe el compilador requerir un acceso inalcanzable returndespués de la llamada a la función que no regresa? El caso en que no es necesario no es muy común, y el requisito de incluirlo incluso en tales casos probablemente no hubiera sido oneroso, pero la decisión de no requerirlo es razonable.
supercat
1
@supercat: un compilador súper inteligente no advertirá ni producirá errores en tal caso, pero, nuevamente, esto es esencialmente incalculable para el caso general, por lo que está atascado con una regla general. Sin embargo, si sabe que nunca se alcanzará su fin de función, entonces está tan lejos de la semántica del manejo tradicional de funciones que, sí, puede seguir adelante y hacer esto y saber que es seguro. Francamente, eres una capa por debajo de C ++ en ese punto y, como tal, todas sus garantías son discutibles de todos modos.
ligereza corre en órbita el
42

gcc no verifica por defecto que todas las rutas de código devuelvan un valor porque, en general, esto no se puede hacer. Asume que sabes lo que estás haciendo. Considere un ejemplo común usando 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 un error, este método siempre devuelve un color. gcc confía en que sabes lo que estás haciendo, por lo que no te obliga a poner un retorno en la parte inferior 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 es obligatorio por la especificación del lenguaje Java. Tenga en cuenta que a veces está mal y debe ingresar 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 manera predeterminada.

John Kugelman
fuente
2
Si javac realmente comprueba las rutas de código, ¿no vería que nunca podría llegar a ese punto?
Chris Lutz
3
En el primero no le da crédito por cubrir todos los casos de enumeración (necesita un caso predeterminado o una devolución después del cambio), y en el segundo no sabe que System.exit()nunca regresa.
John Kugelman
2
Parece sencillo para javac (un compilador de otro modo poderoso) saber que System.exit()nunca regresa. Lo busqué ( java.sun.com/j2se/1.4.2/docs/api/java/lang/… ), y los documentos simplemente dicen que "normalmente nunca regresa". Me pregunto lo que eso significa ...
Paul Biggar
@Paul: significa que no tenían un goodeditor. Todos los demás idiomas dicen "nunca regresa normalmente", es decir, "no regresa utilizando el mecanismo de retorno normal".
Max Lybbert
1
Definitivamente preferiría un compilador que al menos advirtiera si encontraba ese primer ejemplo, porque la corrección de la lógica se rompería si alguien agregara un nuevo valor a la enumeración. Quisiera un caso predeterminado que se quejó en voz alta y / o se bloqueó (probablemente usando una afirmación).
Injektilo
14

Es decir, ¿por qué fluir del final de una función de retorno de valor (es decir, salir sin un explícito return) no es un error?

En primer lugar, en C si una función devuelve algo significativo o no solo es crítico cuando el código de ejecución realmente usa el valor devuelto. Tal vez el lenguaje no quería forzarlo a devolver nada cuando sabe que no lo va a usar de todos modos la mayor parte del tiempo.

En segundo lugar, al parecer, la especificación del lenguaje no quería obligar a los autores del compilador a detectar y verificar todas las rutas de control posibles para la presencia de un código explícito return(aunque en muchos casos esto no es tan difícil de hacer). Además, algunas rutas de control pueden conducir a funciones que no regresan , el rasgo que el compilador generalmente no conoce. Tales caminos pueden convertirse en una fuente de falsos positivos molestos.

Tenga en cuenta también que C y C ++ difieren en sus definiciones del comportamiento en este caso. En C ++, simplemente fluir al final de una función de retorno de valor siempre es un comportamiento indefinido (independientemente de si el código de llamada utiliza el resultado de la función). En C esto provoca un comportamiento indefinido solo si el código de llamada intenta utilizar el valor devuelto.

Hormiga
fuente
+1 pero ¿C ++ no puede omitir returndeclaraciones desde el final de main()?
Chris Lutz
3
@ Chris Lutz: Sí, maines especial en ese sentido.
AnT
5

Es legal bajo C / C ++ no regresar de una función que dice devolver algo. Hay varios casos de uso, como llamar exit(-1)o una función que lo llama o lanza una excepción.

El compilador no va a rechazar C ++ legal incluso si conduce a UB si le está pidiendo que no lo haga. En particular, solicita que no se generen advertencias . (Gcc todavía activa algunos de forma predeterminada, pero cuando se agregan, estos parecen alinearse con las nuevas funciones, no con nuevas advertencias para las funciones antiguas)

Cambiar el gcc predeterminado sin argumentos para emitir algunas advertencias podría ser un cambio importante para los scripts existentes o los sistemas de fabricación. Bien diseñados, ya sea -Wallcon advertencias o alternar advertencias individuales.

Aprender a usar una cadena de herramientas C ++ es una barrera para aprender a ser un programador C ++, pero las cadenas de herramientas C ++ generalmente están escritas por y para expertos.

Yakk - Adam Nevraumont
fuente
Sí, en mi Makefilelo tengo funcionando -Wall -Wpedantic -Werror, pero este fue un script de prueba único al que olvidé proporcionar los argumentos.
Financia la demanda de Mónica
2
Como ejemplo, -Wduplicated-condformar parte de la -Wallrutina de arranque GCC rota. Algunas advertencias que parecen apropiadas en la mayoría de los códigos no lo son en todos los códigos. Es por eso que no están habilitados por defecto.
uh oh, alguien necesita un títere
su primera oración parece estar en contradicción con la cita en la respuesta aceptada "Fluyendo ... comportamiento indefinido ...". ¿O se considera ub "legal"? ¿O quiere decir que no es UB a menos que el (no) valor devuelto se use realmente? Estoy preocupado por el caso de C ++ por cierto
idclev 463035818
@ tobi303 int foo() { exit(-1); }no regresa intde una función que "dice devolver int". Esto es legal en C ++. Ahora, no devuelve nada ; el final de esa función nunca se alcanza. En realidad, llegar al final de foosería un comportamiento indefinido. Ignorando los casos de fin de proceso, int foo() { throw 3.14; }también afirma regresar intpero nunca lo hace.
Yakk - Adam Nevraumont
1
así que supongo que void* foo(void* arg) { pthread_exit(NULL); }está bien por la misma razón (cuando su único uso es a través de pthread_create(...,...,foo,...);)
idclev 463035818
1

Creo que esto se debe al código heredado (C nunca requirió la declaración de retorno, al igual que C ++). Probablemente hay una enorme base de código que se basa en esa "característica". Pero al menos hay una -Werror=return-type bandera en muchos compiladores (incluidos gcc y clang).

d.lozinski
fuente
1

En algunos casos limitados y raros, fluir del final de una función no nula sin devolver un valor podría ser útil. Como el siguiente código específico de MSVC:

double pi()
{
    __asm fldpi
}

Esta función devuelve pi usando el ensamblaje x86. A diferencia del ensamblaje en GCC, no conozco ninguna forma returnde hacerlo para esto sin involucrar gastos generales en el resultado.

Hasta donde yo sé, los compiladores de C ++ convencionales deberían emitir al menos advertencias para código aparentemente inválido. Si hago que el cuerpo esté pi()vacío, GCC / Clang informará una advertencia y MSVC informará un error.

La gente mencionó excepciones y exiten algunas respuestas. Esas no son razones válidas. O bien lanzar una excepción, o llamar exit, será no hacer que la ejecución de la función fluya fuera de la final. Y los compiladores lo saben: escribir una declaración de lanzamiento o llamar exital cuerpo vacío de pi()detendrá cualquier advertencia o error de un compilador.

Yongwei Wu
fuente
MSVC admite específicamente caerse al final de una no voidfunción después de que asm en línea deja un valor en el registro de valor de retorno. (x87 st0en este caso, EAX para entero. Y tal vez xmm0 en una convención de llamada que devuelve flotante / doble en xmm0 en lugar de st0). La definición de este comportamiento es específica de MSVC; ni siquiera sonar -fasm-blockspara soportar la misma sintaxis hace que esto sea seguro. Ver ¿Tiene __asm ​​{}; devolver el valor de eax?
Peter Cordes
0

¿En qué circunstancias no produce un error? Si declara un tipo de retorno y no devuelve algo, me parece un error.

La única excepción que se me ocurre es la main()función, que no necesita una returndeclaración (al menos en C ++; no tengo ninguno de los estándares de C a la mano). Si no hay devolución, actuará como si fuera return 0;la última declaración.

David Thornley
fuente
1
main()necesita una returnen C.
Chris Lutz
@Jefromi: El OP pregunta por una función no nula sin una return <value>;declaración
Bill
2
main automáticamente devuelve 0 en C y C ++. C89 necesita un retorno explícito.
Johannes Schaub - litb
1
@Chris: en C99 hay un implícito return 0;al final de main()(y main()solo). Pero es un buen estilo agregar de return 0;todos modos.
pmg
0

Parece que necesitas subir las advertencias de tu compilador:

$ gcc -Wall -Wextra -Werror -x c -
int main(void) { return; }
cc1: warnings being treated as errors
<stdin>: In function main’:
<stdin>:1: warning: return with no value, in function returning non-void
<stdin>:1: warning: control reaches end of non-void function
$
Chris Lutz
fuente
55
Decir "activar -Werror" no es una respuesta. Claramente, hay una diferencia en la gravedad entre los problemas clasificados como advertencias y errores, y gcc trata a este como la clase menos severa.
Cascabel el
2
@Jefromi: Desde el punto de vista del lenguaje puro, no hay diferencia entre advertencias y errores. El compilador solo está obligado a emitir un "mensaje de diagnóstico". No hay ningún requisito para detener la compilación o llamar a algo "un error" y algo más "una advertencia". Una vez que se emite un mensaje de diagnóstico (o de cualquier tipo), depende totalmente de usted tomar una decisión.
AnT
1
Por otra parte, el problema en cuestión causa UB. Los compiladores no están obligados a atrapar UB en absoluto.
AnT
1
En 6.9.1 / 12 en n1256 dice "Si se alcanza el} que termina una función, y el llamante utiliza el valor de la llamada de función, el comportamiento no está definido".
Johannes Schaub - litb
2
@ Chris Lutz: No lo veo. Es una violación de restricción usar un vacío explícitoreturn; en una función no nula, y es una violación de restricción usar return <value>;en una función nula. Pero ese, creo, no es el tema. El OP, como lo entendí, se trata de salir de una función no nula sin un return(solo permitiendo que el control fluya desde el final de la función). No es una violación de restricción, AFAIK. El estándar solo dice que siempre es UB en C ++ y, a veces, UB en C.
AnT
-2

Es una violación de restricción en c99, pero no en c89. Contraste:

c89:

3.6.6.4 La returndeclaración

Restricciones

Una returndeclaración con una expresión no aparecerá en una función cuyo tipo de retorno sea void.

c99:

6.8.6.4 La returndeclaración

Restricciones

Una returndeclaración con una expresión no aparecerá en una función cuyo tipo de retorno sea void. Una returndeclaración sin una expresión solo aparecerá en una función cuyo tipo de retorno sea void.

Incluso en --std=c99modo, gcc solo lanzará una advertencia (aunque sin necesidad de habilitar -Wbanderas adicionales , como se requiere por defecto o en c89 / 90).

Edite para agregar eso en c89, "alcanzar el }que termina una función es equivalente a ejecutar una returndeclaración sin una expresión" (3.6.6.4). Sin embargo, en c99 el comportamiento es indefinido (6.9.1).

Matt B.
fuente
44
Tenga en cuenta que esto solo cubre returndeclaraciones explícitas . No cubre la caída del final de una función sin devolver un valor.
John Kugelman
3
Tenga en cuenta que C99 falla "alcanzar el} que finaliza una función es equivalente a ejecutar una declaración de retorno sin una expresión" por lo que no se hace una violación de restricción, y por lo tanto no se requiere diagnóstico.
Johannes Schaub - litb