Почему выпадение из конца непустой функции без возврата значения не приводит к ошибке компилятора?

158

С тех пор, как я понял много лет назад, что это не приводит к ошибке по умолчанию (по крайней мере, в GCC), я всегда задавался вопросом, почему?

Я понимаю, что вы можете использовать флаги компилятора для выдачи предупреждения, но не должно ли это быть ошибкой? Почему имеет смысл, чтобы не пустая функция не возвращала значение, чтобы быть действительным?

Пример в соответствии с просьбой в комментариях:

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

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

... компилирует.

Catskul
источник
9
Кроме того, я обрабатываю все предупреждения, как бы тривиально, как ошибки, и активирую все предупреждения, которые могу (с локальной деактивацией, если необходимо ... но тогда в коде ясно, почему).
Матье М.
8
-Werror=return-typeбудет рассматривать только это предупреждение как ошибку. Я просто проигнорировал предупреждение, и пара минут разочарования, выслеживающих неверный thisуказатель, привели меня сюда и к такому выводу.
Jozxyqk
Это усугубляется тем фактом, что поток из конца std::optionalфункции без возврата возвращает «истинное» необязательное значение
Руфус
@Rufus Это не обязательно. Именно это и произошло на вашем компьютере / компиляторе / ОС / лунном цикле. Какой бы нежелательный код не генерировал компилятор из-за неопределенного поведения, он просто выглядел как «истинный» необязательный, что бы это ни было.
underscore_d

Ответы:

146

Стандарты C99 и C ++ не требуют, чтобы функции возвращали значение. Отсутствующий оператор возврата в функции, возвращающей значение, будет определен (возвращаться 0) только в mainфункции.

Обоснование состоит в том, что проверить, возвращает ли каждый путь кода значение, довольно сложно, и возвращаемое значение может быть установлено с помощью встроенного ассемблера или других хитрых методов.

Из C ++ 11 черновик:

§ 6.6.3 / 2

Перетекание из конца функции [...] приводит к неопределенному поведению в функции, возвращающей значение.

§ 3.6.1 / 5

Если контроль достигает конца, mainне встречая return оператора, эффект заключается в выполнении

return 0;

Обратите внимание, что поведение, описанное в C ++ 6.6.3 / 2, отличается от поведения в C.


gcc выдаст вам предупреждение, если вы вызовете его с опцией -Wreturn-type.

Wreturn-type Предупреждать всякий раз, когда функция определена с типом возвращаемого значения, по умолчанию int. Также предупредите о любом операторе возврата без возвращаемого значения в функции, чей тип возврата не является пустым (падение конца тела функции считается возвращаемым без значения), а также об операторе возврата с выражением в функции, Возвращаемый тип void.

Это предупреждение включено -Wall .


Просто для любопытства посмотрите, что делает этот код:

#include <iostream>

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

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

Этот код имеет формально неопределенное поведение, и на практике он вызывает соглашение и архитектуру . В одной конкретной системе с одним конкретным компилятором возвращаемое значение является результатом последней оценки выражения, сохраненной в eaxрегистре процессора этой системы.

Фнието - Фернандо Ньето
источник
13
Я бы с осторожностью назвал неопределенное поведение «разрешенным», хотя, по общему признанию, я бы тоже ошибался, называя это «запрещенным» Не быть ошибкой и не требовать диагностики - это не то же самое, что «разрешено». По крайней мере, ваш ответ выглядит так, как будто вы говорите, что все в порядке, что в основном не так.
Гонки легкости на орбите
4
@ Catskul, почему ты покупаешь этот аргумент? Разве не выполнимо, если не просто, идентифицировать точки выхода функции и убедиться, что все они возвращают значение (и значение объявленного типа возврата)?
BlueBomber
3
@ Catskul, да и нет. Статически типизированные и / или скомпилированные языки делают много вещей, которые вы, вероятно, считаете «чрезмерно дорогими», но поскольку они делают это только один раз, во время компиляции они несущественны. Несмотря на это, я не понимаю, почему определение точек выхода функции должно быть суперлинейным: вы просто пересекаете AST функции и ищете вызовы возврата или выхода. Это линейное время, которое определенно эффективно.
BlueBomber
3
@LightnessRacesinOrbit: Если функция с возвращаемым значением иногда сразу возвращается со значением, а иногда вызывает другую функцию, которая всегда завершается через throwили longjmp, должен ли компилятор требовать недоступность returnпосле вызова невозвратной функции? Случай, когда он не нужен, не очень распространен, и требование включить его даже в таких случаях, вероятно, не было бы обременительным, но решение не требовать его является разумным.
суперкат
1
@supercat: супер-интеллектуальный компилятор не будет предупреждать или выдавать ошибку в таком случае, но - опять же - это существенно не поддается исчислению для общего случая, поэтому вы придерживаетесь общего правила. Однако если вы знаете, что ваш конец функции никогда не будет достигнут, то вы настолько далеки от семантики традиционной обработки функций, что, да, вы можете пойти дальше и сделать это и знать, что это безопасно. Честно говоря, на тот момент вы находитесь на уровне ниже C ++, и, таким образом, все его гарантии в любом случае не имеют значения.
Гонки легкости на орбите
42

По умолчанию gcc не проверяет, что все пути кода возвращают значение, потому что в общем случае это невозможно. Предполагается, что вы знаете, что делаете. Рассмотрим общий пример с использованием перечислений:

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

    // Error, no return?
}

Вы, программист, знаете, что, за исключением ошибки, этот метод всегда возвращает цвет. gcc верит, что вы знаете, что делаете, поэтому не заставляет вас помещать возврат в конец функции.

javac, с другой стороны, пытается проверить, что все пути кода возвращают значение, и выдает ошибку, если не может доказать, что все они делают. Эта ошибка предписана спецификацией языка Java. Обратите внимание, что иногда это неправильно, и вы должны добавить ненужный оператор возврата.

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 и C ++ являются более разрешающими и доверчивыми языками, чем Java или C #, поэтому некоторые ошибки в более новых языках являются предупреждениями в C / C ++, а некоторые предупреждения игнорируются или отключаются по умолчанию.

Джон Кугельман
источник
2
Если javac действительно проверяет пути кода, разве он не увидит, что вы никогда не достигнете этой точки?
Крис Латс
3
В первом из них вы не сможете оценить все случаи перечисления (вам нужен случай по умолчанию или возврат после переключения), а во втором он не знает, что System.exit()никогда не вернется.
Джон Кугельман
2
Javac (в противном случае мощный компилятор) знает, что System.exit()никогда не вернется. Я посмотрел его ( java.sun.com/j2se/1.4.2/docs/api/java/lang/… ), и в документах просто говорится, что он "никогда не возвращается". Интересно, что это значит ...
Пол Биггар
@Paul: это означает, что у них не было goodeditor. Все другие языки говорят: «никогда не возвращается нормально», то есть «не возвращается с использованием нормального механизма возврата».
Макс Либберт
1
Я бы определенно предпочел компилятор, который хотя бы предупредил, если встретится с первым примером, потому что правильность логики нарушится, если кто-нибудь добавит новое значение в перечисление. Я бы хотел случай по умолчанию, который жаловался громко и / или потерпел крах (вероятно, используя утверждение).
Injektilo
14

Вы имеете в виду, почему утечка из конца функции, возвращающей значение (т.е. выход без явного return) не является ошибкой?

Во-первых, в C вопрос о том, возвращает ли функция что-либо значимое или нет, имеет значение только в том случае, если исполняемый код фактически использует возвращаемое значение. Возможно, язык не хотел заставлять вас возвращать что-либо, когда вы знаете, что вы не собираетесь его использовать в большинстве случаев.

Во-вторых, очевидно, языковая спецификация не хотела заставлять авторов компилятора обнаруживать и проверять все возможные пути управления на наличие явного return(хотя во многих случаях это не так сложно сделать). Кроме того, некоторые пути управления могут привести к невозвратным функциям - черта, которая обычно не известна компилятору. Такие пути могут стать источником досадных ложных срабатываний.

Также обратите внимание, что C и C ++ различаются в своих определениях поведения в этом случае. В C ++ просто выход из конца функции, возвращающей значение, всегда является неопределенным поведением (независимо от того, используется ли результат функции вызывающим кодом). В C это вызывает неопределенное поведение, только если вызывающий код пытается использовать возвращаемое значение.

Муравей
источник
+1, но не может C ++ пропустить returnоператоры в конце main()?
Крис Латс
3
@ Крис Лутц: Да, mainособенный в этом отношении.
AnT
5

Под C / C ++ разрешено не возвращаться из функции, которая утверждает, что что-то возвращает. Существует ряд вариантов использования, таких как вызов exit(-1)или функция, которая вызывает его или выдает исключение.

Компилятор не собирается отказываться от легального C ++, даже если он ведет к UB, если вы просите об этом. В частности, вы просите об отсутствии предупреждений . (Gcc по-прежнему включает некоторые по умолчанию, но когда они добавляются, они, похоже, соответствуют новым функциям, а не новым предупреждениям для старых функций)

Изменение по умолчанию no-arg gcc для выдачи некоторых предупреждений может быть серьезным изменением для существующих скриптов или систем. Хорошо продуманные или -Wallимеют дело с предупреждениями, или переключают отдельные предупреждения.

Обучение использованию цепочки инструментов C ++ является препятствием для обучения программированию на C ++, но цепочки инструментов C ++ обычно пишутся экспертами и для них.

Якк - Адам Невраумонт
источник
Да, по моему Makefileу меня это работает -Wall -Wpedantic -Werror, но это был одноразовый тестовый скрипт, которому я забыл предоставить аргументы.
Фонд Моники судебный процесс
2
Как пример, -Wduplicated-condучастие в -Wallзагрузке GCC сломалось. Некоторые предупреждения, которые кажутся подходящими в большинстве кодов, не подходят для всего кода. Вот почему они не включены по умолчанию.
Ой, кому-то нужен щенок
Ваше первое предложение, кажется, противоречит цитате в принятом ответе «Стекает ... неопределенное поведение ...». Или UB считается "законным"? Или вы имеете в виду, что это не UB, если (не) возвращаемое значение фактически используется? Я беспокоюсь о C ++ случае между прочим
idclev 463035818
@ tobi303 int foo() { exit(-1); }не возвращается intиз функции, которая "утверждает, что возвращает int". Это законно в C ++. Теперь он не возвращает ничего ; конец этой функции никогда не достигается. На самом деле достижение конца fooбыло бы неопределенным поведением. Игнорирует случаи завершения процесса, int foo() { throw 3.14; }также утверждает, что возвращается, intно никогда не делает.
Якк - Адам Невраумонт
1
так что я думаю, что void* foo(void* arg) { pthread_exit(NULL); }это хорошо по той же причине (когда он используется только через pthread_create(...,...,foo,...);)
idclev 463035818
1

Я считаю, что это из-за унаследованного кода (C никогда не требовал оператора return, как и C ++). Вероятно, существует огромная кодовая база, опирающаяся на эту «функцию». Но по крайней мере есть -Werror=return-type флаг на многих компиляторах (включая gcc и clang).

d.lozinski
источник
1

В некоторых ограниченных и редких случаях может оказаться полезным вытекание из конца непустой функции без возврата значения. Как следующий код, специфичный для MSVC:

double pi()
{
    __asm fldpi
}

Эта функция возвращает пи с использованием сборки x86. В отличие от сборки в GCC, я не знаю способа использовать returnэто, не задействуя в результате издержки.

Насколько я знаю, основные компиляторы C ++ должны выдавать как минимум предупреждения для явно некорректного кода. Если я сделаю тело pi()пустым, GCC / Clang сообщит о предупреждении, а MSVC сообщит об ошибке.

Люди упоминали об исключениях и exitв некоторых ответах. Это не веские причины. Вызов исключения или вызов неexit приведут к потере выполнения функции. И компиляторы знают это: написание оператора throw или обращения к пустому телу остановит все предупреждения или ошибки от компилятора.exitpi()

Юнвэй Ву
источник
MSVC, в частности, поддерживает опускание конца не- voidфункции после того, как встроенный asm оставляет значение в регистре возвращаемого значения. (x87 st0в данном случае EAX для целого числа. И, возможно, xmm0 в соглашении о вызовах, которое возвращает float / double в xmm0 вместо st0). Определение этого поведения специфично для MSVC; даже не лгать с -fasm-blocksподдержкой того же синтаксиса делает это безопасным. Смотрите ли __asm ​​{}; вернуть значение eax?
Питер Кордес
0

При каких обстоятельствах это не приводит к ошибке? Если он объявляет тип возвращаемого значения и ничего не возвращает, для меня это звучит как ошибка.

Единственное исключение, о котором я могу подумать, - это main()функция, которая вообще не нуждается в returnвыражении (по крайней мере, в C ++; у меня нет под рукой ни одного из стандартов C). Если нет возврата, он будет действовать так, как будто return 0;это последнее утверждение.

Дэвид Торнли
источник
1
main()нужен returnв C.
Крис Латс
@Jefromi: ОП спрашивает о непустой функции без return <value>;заявления
Билл
2
main автоматически возвращает 0 в C и C ++. C89 нуждается в явном возврате.
Йоханнес Шауб - лит
1
@Chris: в C99 есть неявное return 0;в конце main()main()только). Но это хороший стиль, чтобы добавить в return 0;любом случае.
pmg
0

Похоже, вам нужно включить предупреждения вашего компилятора:

$ 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
Сказать «включить -Werror» не ответ. Очевидно, что существует серьезное различие между проблемами, классифицированными как предупреждения и ошибки, и gcc рассматривает этот вопрос как менее серьезный класс.
Каскабель
2
@Jefromi: с чисто языковой точки зрения нет разницы между предупреждениями и ошибками. Компилятор требуется только для выдачи «диагностического сообщения». Нет необходимости останавливать компиляцию или называть что-то «ошибкой», а что-то еще «предупреждением». Выдается одно диагностическое сообщение (или любое другое), и вам остается только принять решение.
AnT
1
Опять же, рассматриваемая проблема вызывает UB. Компиляторы не обязаны ловить UB вообще.
AnT
1
В 6.9.1 / 12 в n1256 говорится: «Если достигнут}, завершающий функцию, и вызывающая сторона использует значение вызова функции, поведение не определено».
Йоханнес Шауб - лит
2
@ Крис Лутц: я этого не вижу. Нарушением ограничения является использование явного пустого значения return;в функции, не являющейся void, и нарушением ограничения, которое следует использовать return <value>;в функции void. Но это, я считаю, не тема. OP, как я понял, предназначен для выхода из не пустой функции без return(просто позволяя элементу управления выходить из конца функции). Это не нарушение ограничений, AFAIK. Стандарт просто говорит, что это всегда UB в C ++ и иногда UB в C.
AnT
-2

Это нарушение ограничений в c99, но не в c89. Контраст:

c89:

3.6.6.4 returnУтверждение

Ограничения

returnЗаявление с выражением не должны появляться в функции которого возвращаемый тип void.

c99:

6.8.6.4 returnУтверждение

Ограничения

returnЗаявление с выражением не должны появляться в функции которого возвращаемый тип void. returnЗаявление без выражения должны появляться только в функции которого возвращаемый тип void.

Даже в --std=c99режиме gcc выдаст только предупреждение (хотя без необходимости включать дополнительные -Wфлаги, как требуется по умолчанию или в c89 / 90).

Отредактируйте, чтобы добавить это в c89, «достижение того, }что завершает функцию, эквивалентно выполнению returnоператора без выражения» (3.6.6.4). Однако в c99 поведение не определено (6.9.1).

Мэтт Б.
источник
4
Обратите внимание, что это касается только явных returnутверждений. Это не покрывает падение функции без возврата значения.
Джон Кугельман
3
Обратите внимание, что C99 пропускает «достижение», который завершает функцию, эквивалентно выполнению оператора возврата без выражения », поэтому он не является нарушением ограничения и, следовательно, не требует диагностики.
Йоханнес Шауб - лит