Чому відтікання з кінця недійсної функції без повернення значення не створює помилки компілятора?


158

З тих пір, як я багато років тому зрозумів, що це не створює помилок за замовчуванням (принаймні в GCC), я завжди цікавився чому?

Я розумію, що ви можете видавати прапори компілятора для створення попередження, але чи не завжди це має бути помилка? Чому має значення недійсна функція, що не повертає значення, що має значення?

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

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

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

... компілює.


9
Крім того, я розглядаю всі попередження, як би тривіальні помилки, і я активую всі попередження, які можу (з місцевою дезактивацією, якщо потрібно ... але тоді в коді зрозуміло, чому).
Матьє М.

8
-Werror=return-typeтрактуватиме саме це попередження як помилку. Я просто проігнорував попередження, і кілька хвилин розчарування відстеження недійсного thisвказівника привели мене сюди і до цього висновку.
jozxyqk

Це погіршується тим, що відтікання кінця std::optionalфункції без повернення повертає "справжню" необов'язковість
Руфус

@Rufus Це не повинно. Це саме те, що трапилося на вашому машині / компіляторі / ОС / місячному циклі. Який би непотрібний код не створював компілятор через невизначену поведінку, він виглядав як "справжній" необов'язковий, що б це не було.
підкреслити_15

Відповіді:


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.

-Ремонт типу Попередження, коли функція визначається з типом повернення, який за замовчуванням є int. Також попередити про будь-який оператор повернення без значення повернення у функції, тип повернення якого не є недійсним (падіння з кінця тіла функції вважається поверненням без значення), і про оператор повернення з виразом у функції, чий тип повернення недійсний.

Це попередження увімкнено функцією -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після виклику функції, що не повертається? Випадок, коли він не потрібен, не дуже поширений, і вимога включити його навіть у таких випадках, ймовірно, не була б обтяжливою, але рішення не вимагати цього є розумним.
supercat

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
Явакові (інакше потужному компілятору) здається просто зрозумілим, що System.exit()ніколи не повертається. Я подивився це ( java.sun.com/j2se/1.4.2/docs/api/java/lang/… ), і документи просто кажуть, що "ніколи зазвичай не повертається". Цікаво, що це означає ...
Пол Біггар

@Paul: це означає, що вони не мали хорошого ревізора. Усі інші мови кажуть, що "ніколи не повертається нормально", тобто "не повертається за допомогою звичайного механізму повернення".
Макс Лібберт

1
Я б напевно віддав перевагу компілятору, який, принаймні, попередив, якщо він стикається з тим першим прикладом, тому що правильність логіки порушиться, якщо хтось додасть новій цінності до перерахунків. Я хочу, щоб випадок за замовчуванням скаржився голосно та / або вийшов з ладу (можливо, використовуючи твердження).
Injektilo

14

Ти маєш на увазі, чому відтікання від кінця функції повернення значення (тобто вихід без явного return) не є помилкою?

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

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

Зауважимо також, що C і C ++ відрізняються своїми визначеннями поведінки в цьому випадку. У C ++ просто витікання з кінця функції повернення значення завжди є невизначеною поведінкою (незалежно від того, чи використовує результат функції код виклику). У C це викликає невизначене поведінку, лише якщо код виклику намагається використовувати повернене значення.


+1, але чи не може C ++ пропустити returnзаяви з кінця main()?
Кріс Лутц

3
@Chris Lutz: Так, 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 ++ btw
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 ніколи не вимагав заяви про повернення, як і C ++). Напевно, величезна база коду спирається на цю "особливість". Але принаймні є -Werror=return-type прапор на багатьох компіляторах (включаючи gcc та clang).


1

У деяких обмежених та рідкісних випадках може бути корисним стікання з кінця недійсної функції без повернення значення. Як і наступний специфічний для MSVC код:

double pi()
{
    __asm fldpi
}

Ця функція повертає pi за допомогою складання x86. На відміну від складання в GCC, я не знаю, як returnце зробити, не включаючи накладні витрати в результат.

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

Люди згадували винятки та exitв деяких відповідях. Це не вагомі причини. Або викидання винятку, або виклик exit, не змусять виконання функції відтікати до кінця. І компілятори це знають: написання заяви про викидання або виклик exitу порожній частині pi()файлу зупинить будь-які попередження чи помилки компілятора.


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
@Chris Lutz: Я цього не бачу. Порушенням обмежень є використання явного порожнього return;у недійсній функції, а порушення обмеження - використання return <value>;у функції пустоти. Але це, я вважаю, не тема. ОП, як я зрозумів, стосується виходу з недійсної функції без return(просто дозволяє керуванню витікати з кінця функції). Це не обмеження обмеження, AFAIK. Стандарт просто говорить, що це завжди UB в C ++, а іноді і UB в C.
квітня 0909

-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 пропускає "досягнення}, що завершує функцію, еквівалентно виконанню оператора повернення без виразу", тому це не допущено порушення обмежень, і тому діагностика не потрібна.
Йоханнес Шауб - ліб
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.