Pourquoi la fin d'une fonction non void sans renvoyer une valeur ne produit-elle pas une erreur du compilateur?

158

Depuis que j'ai réalisé il y a de nombreuses années, que cela ne produisait pas d'erreur par défaut (au moins dans GCC), je me suis toujours demandé pourquoi?

Je comprends que vous pouvez émettre des indicateurs de compilateur pour produire un avertissement, mais cela ne devrait-il pas toujours être une erreur? Pourquoi est-il logique qu'une fonction non vide ne renvoyant pas de valeur soit valide?

Un exemple comme demandé dans les commentaires:

#include <stdio.h>
int stringSize()
{
}

int main()
{
    char cstring[5];
    printf( "the last char is: %c\n", cstring[stringSize()-1] ); 
    return 0;
}

... compile.

Catskul
la source
9
Alternativement, je traite tous les avertissements, même triviaux, comme des erreurs, et j'active tous les avertissements que je peux (avec une désactivation locale si nécessaire ... mais alors c'est clair dans le code pourquoi).
Matthieu M.
8
-Werror=return-typetraitera juste cet avertissement comme une erreur. J'ai simplement ignoré l'avertissement et les quelques minutes de frustration à la recherche d'un thispointeur invalide m'ont conduit ici et à cette conclusion.
jozxyqk
Cela est aggravé par le fait que la sortie de la fin d'une std::optionalfonction sans retourner renvoie un "vrai" facultatif
Rufus
@Rufus Ce n'est pas nécessaire. C'est exactement ce qui s'est passé sur votre machine / compilateur / OS / cycle lunaire. Quel que soit le code indésirable généré par le compilateur à cause du comportement indéfini, il ressemble à un "vrai" facultatif, quoi que ce soit.
underscore_d

Réponses:

146

Les normes C99 et C ++ n'exigent pas que les fonctions retournent une valeur. L'instruction de retour manquante dans une fonction de retour de valeur sera définie (à renvoyer 0) uniquement dans la mainfonction.

La justification inclut que vérifier si chaque chemin de code renvoie une valeur est assez difficile, et une valeur de retour pourrait être définie avec l'assembleur intégré ou d'autres méthodes délicates.

À partir du brouillon C ++ 11 :

§ 6.6.3 / 2

L'écoulement de la fin d'une fonction [...] entraîne un comportement indéfini dans une fonction de retour de valeur.

§ 3.6.1 / 5

Si le contrôle atteint la fin de mainsans rencontrer d' return instruction, l'effet est celui de l'exécution

return 0;

Notez que le comportement décrit dans C ++ 6.6.3 / 2 n'est pas le même en C.


gcc vous donnera un avertissement si vous l'appelez avec l'option -Wreturn-type.

-Wreturn-type Avertir chaque fois qu'une fonction est définie avec un type de retour qui vaut par défaut int. Avertissez également de toute instruction de retour sans valeur de retour dans une fonction dont le type de retour n'est pas void (tomber la fin du corps de la fonction est considéré comme retournant sans valeur), et d'une instruction de retour avec une expression dans une fonction dont return-type est nul.

Cet avertissement est activé par -Wall .


Juste par curiosité, regardez ce que fait ce code:

#include <iostream>

int foo() {
   int a = 5;
   int b = a + 1;
}

int main() { std::cout << foo() << std::endl; } // may print 6

Ce code a un comportement formellement non défini et, en pratique, il appelle la convention et l' architecture dépendante. Sur un système particulier, avec un compilateur particulier, la valeur de retour est le résultat de la dernière évaluation d'expression, stockée dans le eaxregistre du processeur de ce système.

fnieto - Fernando Nieto
la source
13
Je me méfierais d'appeler un comportement indéfini «autorisé», même si j'aurais également tort de l'appeler «interdit». Ne pas être une erreur et ne pas nécessiter de diagnostic n'est pas tout à fait la même chose que «autorisé». À tout le moins, votre réponse se lit un peu comme si vous disiez que vous pouvez le faire, ce qui n'est généralement pas le cas.
Courses de légèreté en orbite le
4
@Catskul, pourquoi achetez-vous cet argument? Ne serait-il pas possible, sinon trivialement, d'identifier les points de sortie d'une fonction et de s'assurer qu'ils renvoient tous une valeur (et une valeur du type de retour déclaré)?
BlueBomber
3
@Catskul, oui et non. Les langages statiquement typés et / ou compilés font beaucoup de choses que vous considérez probablement comme «excessivement chères», mais comme ils ne le font qu'une fois, au moment de la compilation, ils ont des dépenses négligeables. Cela dit, je ne vois pas pourquoi l'identification des points de sortie d'une fonction doit être super-linéaire: vous traversez simplement l'AST de la fonction et recherchez les appels de retour ou de sortie. C'est du temps linéaire, qui est décidément efficace.
BlueBomber
3
@LightnessRacesinOrbit: Si une fonction avec une valeur de retour retourne parfois immédiatement avec une valeur et appelle parfois une autre fonction qui sort toujours via throwou longjmp, le compilateur devrait-il exiger un injoignable returnsuite à l'appel de la fonction non retournée? Le cas où cela n'est pas nécessaire n'est pas très courant, et l'exigence de l'inclure même dans de tels cas n'aurait probablement pas été onéreuse, mais la décision de ne pas l'exiger est raisonnable.
supercat le
1
@supercat: Un compilateur super intelligent ne signalera pas ou n'erreur dans un tel cas, mais - encore une fois - c'est essentiellement incalculable pour le cas général, vous êtes donc coincé avec une règle générale. Si vous savez, cependant, que votre fin de fonction ne sera jamais atteinte, alors vous êtes si loin de la sémantique de la gestion des fonctions traditionnelle que, oui, vous pouvez continuer et faire cela et savoir que c'est sûr. Franchement, vous êtes une couche sous C ++ à ce stade et, en tant que tel, toutes ses garanties sont de toute façon discutables.
Courses de légèreté en orbite le
42

gcc ne vérifie pas par défaut que tous les chemins de code renvoient une valeur car en général cela ne peut pas être fait. Cela suppose que vous savez ce que vous faites. Prenons un exemple courant utilisant des énumérations:

Color getColor(Suit suit) {
    switch (suit) {
        case HEARTS: case DIAMONDS: return RED;
        case SPADES: case CLUBS:    return BLACK;
    }

    // Error, no return?
}

Vous le programmeur savez que, sauf bogue, cette méthode renvoie toujours une couleur. gcc croit que vous savez ce que vous faites afin de ne pas vous obliger à mettre un retour au bas de la fonction.

javac, d'autre part, essaie de vérifier que tous les chemins de code renvoient une valeur et renvoie une erreur s'il ne peut pas prouver qu'ils le font tous. Cette erreur est imposée par la spécification du langage Java. Notez que parfois c'est faux et que vous devez mettre une déclaration de retour inutile.

char getChoice() {
    int ch = read();

    if (ch == -1 || ch == 'q') {
        System.exit(0);
    }
    else {
        return (char) ch;
    }

    // Cannot reach here, but still an error.
}

C'est une différence philosophique. C et C ++ sont des langages plus permissifs et plus fiables que Java ou C # et certaines erreurs dans les nouveaux langages sont des avertissements en C / C ++ et certains avertissements sont ignorés ou désactivés par défaut.

John Kugelman
la source
2
Si javac vérifie réellement les chemins de code, ne verrait-il pas que vous ne pourriez jamais atteindre ce point?
Chris Lutz
3
Dans le premier, il ne vous donne pas de crédit pour couvrir tous les cas d'énumération (vous avez besoin d'un cas par défaut ou d'un retour après le changement), et dans le second, il ne sait pas qu'il System.exit()ne revient jamais.
John Kugelman
2
Il semble simple pour javac (un compilateur par ailleurs puissant) de savoir que cela System.exit()ne revient jamais. Je l'ai recherché ( java.sun.com/j2se/1.4.2/docs/api/java/lang/… ), et la documentation dit simplement qu'il "ne retourne jamais normalement". Je me demande ce que cela signifie ...
Paul Biggar
@Paul: cela signifie qu'ils n'avaient pas de bon rédacteur. Tous les autres langages disent "ne retourne jamais normalement" - c'est-à-dire "ne retourne pas en utilisant le mécanisme de retour normal".
Max Lybbert
1
Je préférerais définitivement un compilateur qui avertit au moins s'il rencontrait ce premier exemple, car l'exactitude de la logique serait rompue si quelqu'un ajoutait une nouvelle valeur à l'énumération. Je voudrais un cas par défaut qui s'est plaint bruyamment et / ou s'est écrasé (probablement en utilisant une assertion).
Injektilo
14

Vous voulez dire, pourquoi sortir de la fin d'une fonction de retour de valeur (c'est-à-dire sortir sans explicite return) n'est pas une erreur?

Premièrement, en C, le fait qu'une fonction retourne quelque chose de significatif ou non n'est critique que lorsque le code en cours d'exécution utilise réellement la valeur retournée. Peut-être que la langue ne voulait pas vous forcer à renvoyer quoi que ce soit quand vous savez que vous n'allez pas l'utiliser de toute façon la plupart du temps.

Deuxièmement, apparemment, la spécification du langage ne voulait pas forcer les auteurs du compilateur à détecter et à vérifier tous les chemins de contrôle possibles pour la présence d'un explicite return(bien que dans de nombreux cas ce ne soit pas si difficile à faire). De plus, certains chemins de contrôle peuvent conduire à des fonctions non retournées - le trait généralement inconnu du compilateur. De tels chemins peuvent devenir une source de faux positifs ennuyeux.

Notez également que C et C ++ diffèrent dans leurs définitions du comportement dans ce cas. En C ++, le simple fait d'écouler la fin d'une fonction retournant une valeur est toujours un comportement indéfini (indépendamment du fait que le résultat de la fonction soit utilisé par le code appelant). En C, cela provoque un comportement indéfini uniquement si le code appelant essaie d'utiliser la valeur renvoyée.

Fourmi
la source
+1 mais le C ++ ne peut-il pas omettre les returninstructions de la fin de main()?
Chris Lutz
3
@Chris Lutz: Oui, mainc'est spécial à cet égard.
Du
5

Il est légal sous C / C ++ de ne pas revenir d'une fonction qui prétend retourner quelque chose. Il existe un certain nombre de cas d'utilisation, tels que l'appel exit(-1)ou une fonction qui l'appelle ou lève une exception.

Le compilateur ne rejettera pas le C ++ légal même s'il conduit à UB si vous lui demandez de ne pas le faire. En particulier, vous demandez qu'aucun avertissement ne soit généré. (Gcc en active toujours certains par défaut, mais une fois ajoutés, ceux-ci semblent correspondre aux nouvelles fonctionnalités et non aux nouveaux avertissements pour les anciennes fonctionnalités)

Changer le gcc no-arg par défaut pour émettre des avertissements pourrait être un changement radical pour les scripts existants ou créer des systèmes. Ceux qui sont bien conçus -Wallet traitent les avertissements ou basculent les avertissements individuels.

Apprendre à utiliser une chaîne d'outils C ++ est un obstacle à l'apprentissage du programmeur C ++, mais les chaînes d'outils C ++ sont généralement écrites par et pour des experts.

Yakk - Adam Nevraumont
la source
Ouais, dans mon Makefileje l'ai en cours d'exécution avec -Wall -Wpedantic -Werror, mais c'était un script de test unique que j'ai oublié de fournir les arguments.
Fund Monica's Lawsuit
2
Par exemple, faire -Wduplicated-condpartie d'un -Wallbootstrap cassé de GCC. Certains avertissements qui semblent appropriés dans la plupart des codes ne sont pas appropriés dans tous les codes. C'est pourquoi ils ne sont pas activés par défaut.
uh oh quelqu'un a besoin d'un chiot
votre première phrase semble être en contradiction avec la citation de la réponse acceptée «S'écouler… comportement indéfini…». Ou est-ce que ub est considéré comme "légal"? Ou voulez-vous dire que ce n'est pas UB à moins que la valeur (non) retournée ne soit réellement utilisée? Je suis inquiet pour le cas C ++ btw
idclev 463035818
@ tobi303 int foo() { exit(-1); }ne retourne pas intd'une fonction qui "prétend retourner int". Ceci est légal en C ++. Maintenant, cela ne renvoie rien ; la fin de cette fonction n'est jamais atteinte. En fait, atteindre la fin fooserait un comportement indéfini. Ignorer les cas de fin de processus, int foo() { throw 3.14; }prétend également revenir intmais ne le fait jamais.
Yakk - Adam Nevraumont
1
donc je suppose que void* foo(void* arg) { pthread_exit(NULL); }c'est bien pour la même raison (quand sa seule utilisation est via pthread_create(...,...,foo,...);)
idclev 463035818
1

Je pense que c'est à cause du code hérité (C n'a jamais requis de déclaration de retour, tout comme C ++). Il existe probablement une énorme base de code reposant sur cette «fonctionnalité». Mais au moins il y a un -Werror=return-type drapeau sur de nombreux compilateurs (y compris gcc et clang).

d.lozinski
la source
1

Dans certains cas limités et rares, la sortie de la fin d'une fonction non vide sans renvoyer une valeur peut être utile. Comme le code spécifique MSVC suivant:

double pi()
{
    __asm fldpi
}

Cette fonction renvoie pi en utilisant l'assembly x86. Contrairement à l'assemblage dans GCC, je ne connais aucun moyen d'utiliserreturn pour faire cela sans impliquer des frais généraux dans le résultat.

Autant que je sache, les compilateurs C ++ traditionnels devraient émettre au moins des avertissements pour un code apparemment invalide. Si je fais le corps depi() vide, GCC / Clang rapportera un avertissement, et MSVC rapportera une erreur.

Les gens ont mentionné des exceptions et exitdans certaines réponses. Ce ne sont pas des raisons valables. Le fait de lever une exception ou d'appeler neexit fera pas que l'exécution de la fonction se termine. Et les compilateurs le savent: écrire une instruction throw ou appeler exitle corps vide de pi()arrêtera tout avertissement ou erreur d'un compilateur.

Yongwei Wu
la source
MSVC prend spécifiquement en charge la chute de la fin d'une non- voidfonction après que inline asm laisse une valeur dans le registre de valeur de retour. (x87 st0dans ce cas, EAX pour entier. Et peut-être xmm0 dans une convention d'appel qui renvoie float / double en xmm0 au lieu de st0). La définition de ce comportement est spécifique à MSVC; même pas clang avec -fasm-blockspour supporter la même syntaxe rend cela sûr. Voir Does __asm ​​{}; retourner la valeur de eax?
Peter Cordes le
0

Dans quelles circonstances cela ne produit-il pas une erreur? S'il déclare un type de retour et ne renvoie rien, cela me semble être une erreur.

La seule exception à laquelle je peux penser est la main()fonction, qui n'a pas du tout besoin d'une returndéclaration (au moins en C ++; je n'ai aucune des normes C à portée de main). S'il n'y a pas de retour, il agira comme si c'était return 0;la dernière instruction.

David Thornley
la source
1
main()a besoin d'un returnen C.
Chris Lutz
@Jefromi: L'OP pose des questions sur une fonction non annulée sans return <value>;déclaration
Projet de loi
2
main renvoie automatiquement 0 en C et C ++. C89 a besoin d'un retour explicite.
Johannes Schaub - litb
1
@Chris: en C99, il y a un implicite return 0;à la fin de main()(et main()seulement). Mais c'est un bon style à ajouter return 0;quand même.
pmg
0

On dirait que vous devez activer les avertissements de votre compilateur:

$ 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
la source
5
Dire "allumer -Werror" est une non-réponse. Il existe clairement une différence de gravité entre les problèmes classés comme avertissements et erreurs, et gcc traite celui-ci comme la classe la moins grave.
Cascabel
2
@Jefromi: Du point de vue du langage pur, il n'y a aucune différence entre les avertissements et les erreurs. Le compilateur est seulement requis pour émettre un "message disgnostique". Il n'y a aucune obligation d'arrêter la compilation ou d'appeler quelque chose "une erreur" et autre chose "un avertissement". Lorsqu'un message de diagnostic est émis (ou de quelque type que ce soit), il vous appartient entièrement de prendre une décision.
Du
1
Là encore, le problème en question provoque UB. Les compilateurs ne sont pas du tout tenus de capturer UB.
Du
1
Dans 6.9.1 / 12 dans n1256 il dit "Si le} qui termine une fonction est atteint et que la valeur de l'appel de fonction est utilisée par l'appelant, le comportement est indéfini."
Johannes Schaub - litb
2
@Chris Lutz: Je ne le vois pas. C'est une violation de contrainte d'utiliser un vide explicitereturn; dans une fonction non-void, et c'est une violation de contrainte à utiliser return <value>;dans une fonction void. Mais ce n'est pas le sujet, je crois. L'OP, tel que je l'ai compris, consiste à quitter une fonction non vide sans a return(simplement permettre au contrôle de s'écouler à la fin de la fonction). Ce n'est pas une violation de contrainte, AFAIK. La norme dit simplement que c'est toujours UB en C ++ et parfois UB en C.
AnT
-2

Il s'agit d'une violation de contrainte dans c99, mais pas dans c89. Contraste:

c89:

3.6.6.4 Le return déclaration

Contraintes

Une returninstruction avec une expression ne doit pas apparaître dans une fonction dont le type de retour est void.

c99:

6.8.6.4 La returndéclaration

Contraintes

Une returninstruction avec une expression ne doit pas apparaître dans une fonction dont le type de retour est void. Une returninstruction sans expression n'apparaîtra que dans une fonction dont le type de retour est void.

Même en --std=c99mode, gcc ne lancera qu'un avertissement (bien que sans avoir besoin d'activer des -Windicateurs supplémentaires , comme cela est requis par défaut ou dans c89 / 90).

Modifier pour ajouter qu'en c89, "atteindre le }qui termine une fonction équivaut à exécuter une returninstruction sans expression" (3.6.6.4). Cependant, dans c99, le comportement n'est pas défini (6.9.1).

Matt B.
la source
4
Notez que cela ne couvre que les returndéclarations explicites . Il ne couvre pas la chute de la fin d'une fonction sans renvoyer une valeur.
John Kugelman
3
Notez que C99 manque "atteindre le} qui termine une fonction équivaut à exécuter une instruction de retour sans expression" donc il n'est pas fait une violation de contrainte, et donc aucun diagnostic n'est requis.
Johannes Schaub - litb