Por que sair do final de uma função não nula sem retornar um valor não produz um erro do compilador?


158

Desde que percebi, há muitos anos, que isso não produz um erro por padrão (pelo menos no GCC), sempre me perguntei por que?

Entendo que você pode emitir sinalizadores de compilador para gerar um aviso, mas isso não deveria sempre ser um erro? Por que faz sentido que uma função não nula não retorne um valor para ser válida?

Um exemplo conforme solicitado nos comentários:

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

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

... compila.


9
Como alternativa, trato todos os avisos de maneira trivial, como erros, e ativo todos os avisos que posso (com desativação local, se necessário ... mas fica claro no código).
Matthieu M.

8
-Werror=return-typetratará exatamente esse aviso como um erro. Eu apenas ignorei o aviso e os dois minutos de frustração ao localizar um thisponteiro inválido me levaram até aqui e a esta conclusão.
jozxyqk

Isso é agravado pelo fato de que sair do final de uma std::optionalfunção sem retornar retorna uma opção "verdadeira"
Rufus

@Rufus Não precisa. Foi exatamente o que aconteceu no seu ciclo de máquina / compilador / OS / lunar. Qualquer que seja o código indesejado que o compilador gerou por causa do comportamento indefinido passou a parecer um opcional "verdadeiro", qualquer que seja esse.
underscore_d

Respostas:


146

Os padrões C99 e C ++ não requerem funções para retornar um valor. A declaração de retorno ausente em uma função de retorno de valor será definida (para retornar 0) apenas na mainfunção.

A lógica inclui que verificar se todo caminho de código retorna um valor é bastante difícil e um valor de retorno pode ser definido com assembler incorporado ou outros métodos complicados.

Do rascunho do C ++ 11 :

§ 6.6.3 / 2

A saída do final de uma função [...] resulta em um comportamento indefinido em uma função de retorno de valor.

§ 3.6.1 / 5

Se o controle chegar ao fim mainsem encontrar uma return declaração, o efeito é o de executar

return 0;

Observe que o comportamento descrito em C ++ 6.6.3 / 2 não é o mesmo em C.


O gcc emitirá um aviso se você chamá-lo com a opção -Wreturn-type.

-Wreturn-type Warn sempre que uma função é definida com um tipo de retorno cujo padrão é int. Também avise sobre qualquer declaração de retorno sem valor de retorno em uma função cujo tipo de retorno não seja nulo (cair do final do corpo da função é considerado retornar sem valor) e sobre uma declaração de retorno com uma expressão em uma função cuja o tipo de retorno é nulo.

Este aviso é ativado por -Wall .


Como curiosidade, veja o que esse código faz:

#include <iostream>

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

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

Esse código possui um comportamento formalmente indefinido e, na prática, está chamando de convenção e arquitetura dependente. Em um sistema específico, com um compilador específico, o valor de retorno é o resultado da avaliação da última expressão, armazenada no eaxregistro do processador desse sistema.


13
Eu seria cauteloso em chamar comportamento indefinido de "permitido", apesar de admitir que também estaria errado em chamar de "proibido". Não ser um erro e não exigir um diagnóstico não é exatamente o mesmo que "permitido". No mínimo, sua resposta é um pouco como se você estivesse dizendo que não há problema em fazer, o que em grande parte não é.
Lightness Races in Orbit

4
@ Catskul, por que você compra esse argumento? Não seria possível, se não trivialmente fácil, identificar os pontos de saída de uma função e garantir que todos retornassem um valor (e um valor do tipo de retorno declarado)?
BlueBomber

3
@ Catskul, sim e não. Linguagens de tipo estatístico e / ou compiladas fazem muitas coisas que você provavelmente consideraria "proibitivamente caras", mas como elas fazem isso apenas uma vez, no momento da compilação, têm despesas desprezíveis. Mesmo tendo dito isso, não vejo por que a identificação dos pontos de saída de uma função precisa ser super-linear: basta percorrer o AST da função e procurar por chamadas de retorno ou saída. Esse é o tempo linear, que é decididamente eficiente.
BlueBomber

3
@LightnessRacesinOrbit: Se uma função com um valor de retorno às vezes retorna imediatamente com um valor e às vezes chama outra função que sempre sai via throwou longjmp, o compilador deve exigir um inalcançável returnapós a chamada para a função que não retorna? O caso em que não é necessário não é muito comum, e um requisito para incluí-lo mesmo nesses casos provavelmente não teria sido oneroso, mas a decisão de não requerê-lo é razoável.
Supercat 5/09/13

1
@ supercat: Um compilador super-inteligente não avisa ou erro nesse caso, mas - novamente - isso é essencialmente incalculável para o caso geral, então você está preso a uma regra geral. Se você sabe, porém, que seu fim de função nunca será alcançado, você está tão longe da semântica do manuseio tradicional de funções que, sim, você pode prosseguir e fazer isso e saber que é seguro. Francamente, você é uma camada abaixo de C ++ nesse ponto e, como tal, todas as suas garantias são discutíveis de qualquer maneira.
Lightness Races em Órbita

42

Por padrão, o gcc não verifica se todos os caminhos de código retornam um valor porque, em geral, isso não pode ser feito. Assume que você sabe o que está fazendo. Considere um exemplo comum usando enumerações:

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

    // Error, no return?
}

Você, o programador, sabe que, salvo um erro, esse método sempre retorna uma cor. O gcc confia que você sabe o que está fazendo, para não forçar você a colocar um retorno na parte inferior da função.

javac, por outro lado, tenta verificar se todos os caminhos de código retornam um valor e lançam um erro se não puder provar que todos o fazem. Este erro é obrigatório pela especificação da linguagem Java. Observe que algumas vezes está errado e você deve inserir uma declaração de retorno desnecessária.

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

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

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

É uma diferença filosófica. C e C ++ são linguagens mais permissivas e confiáveis ​​do que Java ou C # e, portanto, alguns erros nas linguagens mais recentes são avisos em C / C ++ e alguns avisos são ignorados ou desativados por padrão.


2
Se o javac realmente verificar os caminhos de código, não veria que você nunca chegaria a esse ponto?
22720 Chris Lutz

3
No primeiro, não dá crédito por cobrir todos os casos de enum (você precisa de um caso padrão ou de um retorno após a troca), e no segundo, ele não sabe que System.exit()nunca retorna.
22610 John Kugelman

2
Parece simples para o javac (um compilador poderoso) saber que System.exit()nunca retorna. Eu procurei ( java.sun.com/j2se/1.4.2/docs/api/java/lang/… ), e os documentos dizem apenas que "nunca retorna normalmente". Eu me pergunto o que isso significa ...
Paul Biggar

@ Paul: significa que eles não tinham um bom editor. Todos os outros idiomas dizem que "nunca retorna normalmente" - ou seja, "não retorna usando o mecanismo de retorno normal".
Max Lybbert

1
Eu definitivamente preferiria um compilador que pelo menos avisasse se encontrasse o primeiro exemplo, porque a correção da lógica seria interrompida se alguém adicionasse um novo valor ao enum. Eu gostaria de um caso padrão que reclamou alto e / ou travou (provavelmente usando uma afirmação).
Injektilo 13/08/2015

14

Você quer dizer, por que sair do final de uma função de retorno de valor (ou seja, sair sem um explícito return) não é um erro?

Primeiramente, em C, se uma função retorna algo significativo ou não é apenas crítica quando o código em execução realmente usa o valor retornado. Talvez o idioma não queira forçá-lo a devolver nada quando você sabe que não vai usá-lo de qualquer maneira na maioria das vezes.

Segundo, aparentemente a especificação da linguagem não queria forçar os autores do compilador a detectar e verificar todos os caminhos de controle possíveis para a presença de um explícito return(embora em muitos casos isso não seja tão difícil de executar). Além disso, alguns caminhos de controle podem levar a funções que não retornam - a característica que geralmente não é conhecida pelo compilador. Esses caminhos podem se tornar uma fonte de falsos positivos irritantes.

Observe também que C e C ++ diferem em suas definições de comportamento nesse caso. No C ++, apenas sair do final de uma função que retorna valor é sempre um comportamento indefinido (independentemente de o resultado da função ser usado pelo código de chamada). Em C, isso causa comportamento indefinido apenas se o código de chamada tentar usar o valor retornado.


+1, mas o C ++ não pode omitir returninstruções no final de main()?
22711 Chris Lutz

3
@ Chris Lutz: Sim, mainé especial a esse respeito.
22/10/09

5

É legal no C / C ++ não retornar de uma função que afirma retornar algo. Existem vários casos de uso, como chamada exit(-1), ou uma função que chama ou lança uma exceção.

O compilador não rejeitará C ++ legal, mesmo que leve ao UB, se você estiver pedindo para não fazê-lo. Em particular, você está solicitando que nenhum aviso seja gerado. (O Gcc ainda ativa alguns por padrão, mas quando adicionados, esses parecem estar alinhados com novos recursos, não com novos avisos para recursos antigos)

Alterar o no-arg gcc padrão para emitir alguns avisos pode ser uma mudança de ponta para scripts existentes ou criar sistemas. Os bem projetados também -Walle lidam com avisos ou alternam avisos individuais.

Aprender a usar uma cadeia de ferramentas C ++ é uma barreira para aprender a ser um programador de C ++, mas as cadeias de ferramentas C ++ geralmente são escritas por e para especialistas.


Sim, no meu Makefilecaso -Wall -Wpedantic -Werror, mas este foi um script de teste único que eu esqueci de fornecer os argumentos.
Fund Monica's Lawsuit

2
Como exemplo, fazer -Wduplicated-condparte da -Wallinicialização do GCC interrompida. Alguns avisos que parecem adequados na maioria dos códigos não são adequados em todo o código. É por isso que eles não estão ativados por padrão.
ah, alguém precisa de um aluno

sua primeira frase parece estar em contradição com a citação na resposta aceita "Flowing off .... comportamento indefinido ...". Ou ub é considerado "legal"? Ou você quer dizer que não é UB, a menos que o valor (não) retornado seja realmente usado? Estou preocupado com o caso C ++ btw
idclev 463035818

@ tobi303 int foo() { exit(-1); }não retorna intde uma função que "afirma retornar int". Isso é legal em C ++. Agora, ele não retorna nada ; o fim dessa função nunca é alcançado. Na verdade, chegar ao fim fooseria um comportamento indefinido. Ignorar casos de final de processo int foo() { throw 3.14; }também afirma retornar, intmas nunca retorna .
Yakk - Adam Nevraumont

1
Então eu acho que void* foo(void* arg) { pthread_exit(NULL); }é bom para a mesma razão (quando o seu uso apenas é via pthread_create(...,...,foo,...);)
idclev 463035818

1

Eu acredito que isso é devido ao código legado (C nunca exigiu declaração de retorno, assim como C ++). Provavelmente existe uma enorme base de código contando com esse "recurso". Mas pelo menos existe -Werror=return-type sinalizador em muitos compiladores (incluindo gcc e clang).


1

Em alguns casos limitados e raros, pode ser útil sair do final de uma função não nula sem retornar um valor. Como o seguinte código específico da MSVC:

double pi()
{
    __asm fldpi
}

Esta função retorna pi usando assembly x86. Ao contrário da montagem no GCC, não conheço nenhuma maneira returnde fazer isso sem envolver a sobrecarga no resultado.

Tanto quanto eu sei, os compiladores C ++ convencionais devem emitir pelo menos avisos para código aparentemente inválido. Se eu pi()esvaziar o corpo , o GCC / Clang relatará um aviso e a MSVC reportará um erro.

As pessoas mencionaram exceções e exitem algumas respostas. Essas não são razões válidas. Quer lançar uma exceção, ou chamar exit, vai não fazer fluir a execução da função fora da final. E os compiladores sabem disso: escrever uma declaração throw ou chamar exito corpo vazio de pi()interromperá quaisquer avisos ou erros de um compilador.


O MSVC suporta especificamente a queda do final de uma não voidfunção depois que o asm inline deixa um valor no registro de valor de retorno. (x87 st0nesse caso, EAX para inteiro. E talvez xmm0 em uma convenção de chamada que retorne float / double em xmm0 em vez de st0). Definir esse comportamento é específico para MSVC; nem mesmo se apegar -fasm-blocksa suportar a mesma sintaxe torna isso seguro. Veja Does __asm ​​{}; retornar o valor de eax?
Peter Cordes

0

Sob que circunstâncias não produz um erro? Se ele declara um tipo de retorno e não retorna algo, parece um erro para mim.

A única exceção em que consigo pensar é a main()função, que não precisa de nenhuma returndeclaração (pelo menos em C ++; não tenho nenhum dos padrões C à mão). Se não houver retorno, ele atuará como se return 0;fosse a última declaração.


1
main()precisa de um returnC.
Chris Lutz

@Jefromi: O OP é perguntando sobre uma função não-vazio sem uma return <value>;declaração
Bill

2
main retorna automaticamente 0 em C e C ++. C89 precisa de um retorno explícito.
Johannes Schaub - litb

1
@ Chris: em C99, há um implícito return 0;no final de main()(e main()somente). Mas é bom estilo para adicionar de return 0;qualquer maneira.
pmg

0

Parece que você precisa ativar os avisos do 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
$

5
Dizer "ativar -Werror" é uma não resposta. Claramente, há uma diferença de gravidade entre os problemas classificados como avisos e erros, e o gcc trata esse como a classe menos grave.
Cascabel

2
@ Jeffromi: Do ponto de vista da linguagem pura, não há diferença entre avisos e erros. O compilador é necessário apenas para emitir uma "mensagem de diagnóstico". Não há necessidade de interromper a compilação ou chamar algo de "um erro" e outra coisa de "um aviso". Quando uma mensagem de diagnóstico é emitida (ou qualquer outro tipo), é de sua inteira responsabilidade tomar uma decisão.
22/10/09

1
Então, novamente, o problema em questão causa UB. Compiladores não são necessários para capturar UB.
22/10/09

1
Em 6.9.1 / 12 no n1256, diz "Se o} que encerra uma função for atingido, e o valor da chamada da função for usado pelo chamador, o comportamento será indefinido".
Johannes Schaub - litb 22/10/09

2
@ Chris Lutz: Eu não vejo isso. É uma violação de restrição usar um vazio explícitoreturn; em uma função nula e é uma violação de restrição a ser usada return <value>;em uma função nula. Mas acredito que esse não é o assunto. O OP, como eu o entendi, é sobre sair de uma função não nula sem a return(apenas permitindo que o controle flua para o final da função). Não é uma violação de restrição, AFAIK. O padrão apenas diz que é sempre UB em C ++ e, às vezes, UB em C. #
2282

-2

É uma violação de restrição em c99, mas não em c89. Contraste:

c89:

3.6.6.4 A returndeclaração

Restrições

Uma returndeclaração com uma expressão não deve aparecer em uma função cujo tipo de retorno seja void.

c99:

6.8.6.4 O return declaração

Restrições

Uma returndeclaração com uma expressão não deve aparecer em uma função cujo tipo de retorno seja void. Uma returndeclaração sem uma expressão deve aparecer apenas em uma função cujo tipo de retorno seja void.

Mesmo no --std=c99modo, o gcc emitirá apenas um aviso (embora sem a necessidade de ativar-W sinalizadores , conforme exigido por padrão ou no c89 / 90).

Edite para adicionar isso em c89, "atingir a }função que encerra uma função é equivalente a executar uma returninstrução sem uma expressão" (3.6.6.4). No entanto, em c99, o comportamento é indefinido (6.9.1).


4
Observe que isso cobre apenas returndeclarações explícitas . Ele não cobre cair no final de uma função sem retornar um valor.
22610 John Kugelman

3
Observe que C99 erra "atingir o} que encerra uma função é equivalente a executar uma instrução de retorno sem uma expressão", portanto, não se trata de uma violação de restrição e, portanto, nenhum diagnóstico é necessário.
Johannes Schaub - litb 22/10/09
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.