Функции C и С++ без оператора return

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

Я написал следующий тест и запустил его с помощью моего компилятора (gcc (Homebrew gcc 5.2.0) 5.2.0)

#include <stdio.h>

int f(int a, int b) {
  int c = a + b;
}

int main() {
   int x = 5, y = 6;

   printf("f(%d,%d) is %d\n", x, y, f(x,y)); // f(5,6) is 11

   return 0;
}

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

Я нашел этот вопрос, но не был удовлетворен ответом. Я знаю, что с помощью -Wall -Werror этого поведения можно избежать, но почему это вариант? Почему это все еще разрешено?

Ответ 1

"Почему это все еще разрешено?" Это не так, но в целом компилятор не может доказать, что вы это делаете. Рассмотрим этот (конечно, чрезвычайно упрощенный) пример:

// Input is always true because logic reason
int fun (bool b) {
    if (b)
        return 7;
}

Или даже этот:

int fun (bool b) {
    if (b)
        return 7;
    // Defined in a different translation unit, will always call exit()
    foo();
    // Now we can never get here, but the compiler cannot know
}

Теперь первый пример может истечь конец, но он никогда не будет, пока функция будет использоваться "правильно"; а второй не мог, но компилятор не может этого знать. Таким образом, компилятор сломал бы "рабочий" и юридический, хотя, вероятно, глупый код, сделав это ошибкой.

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

Но в конце концов, течет от конца функции void по-прежнему является undefined. Это может работать на некоторых компиляторах, но это не так и никогда не гарантировалось.

Ответ 2

Это не ошибка в компиляторе, но код явно плохо сформирован. на архитектуре x86, (R | E) AX используется как регистр накопителя, а также для возвращаемых значений.

Итак, давайте взглянем на полностью неоптимизированную разборку: https://goo.gl/TihXpa

  • Вы увидите, что код для f() действительно использует eax для хранения результата сложения.

  • Однако... продолжайте использовать -O1 (или любой уровень оптимизации, который более агрессивен) как опция компилятора (верхний правый квадрат) и посмотрите, что происходит сейчас.

... Мы находим, что компилятор правильно понял, что функция явно не возвращает результат, и он просто становится не-оператором.

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

Ответ 3

Результат функции с возвращаемым типом, но без оператора возврата undefined (кроме С++ с main, где возвращаемое значение равно 0). Это также не синтаксическая ошибка, так как в грамматике используется общая форма для функции, которая работает как для функций ретуширования, так и для функции void.

Скорее всего, здесь происходит то, что результат последнего выражения сохраняется в том же регистре (на x86, EAX или RAX), который используется компилятором для возвращаемого значения функции. Когда функция возвращается, команда return оставляет этот регистр отдельно, а вызывающий код просто принимает значение, возвращающее значение функции.

Ответ 4

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

Вот он:

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

Проверьте здесь аналогичную тему.

Ответ 5

На практике возвращаемое значение, вероятно, является последним значением в каком-либо машинном регистре (например, используемом для вычисления c). Однако в соответствии со стандартами как возвращаемое значение, так и результат, если вызывающий абонент обращается к/пользователям, возвращаемое значение undefined.

Что касается того, почему разрешена такая конструкция оскорбительного/уродливого кода.... Во-первых, в ранних версиях c не было ключевого слова void, и - если не указан тип возврата - тип возврата для функция была int. Для функций, которые на самом деле были предназначены для возврата ничего, метод должен был (неявно или явно) определять их как возвращающие int, а не возвращать какое-либо значение, и чтобы вызывающий объект не пытался получить/использовать возвращаемое значение.

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

Позже, даже когда рассматриваемые поставщики компиляции попытались изменить поведение (например, чтобы выпустить сообщение об ошибке и отклонить код, когда функция "упала до конца" ), они получили отчеты об ошибках от разработчиков (некоторые из которых были довольно вокальными) о том, что их программы больше не работают. Как бы то ни было, поставщики компиляторов оказались в стороне от давления. Другие поставщики компиляции также отступили от подобных отчетов об ошибках (в форме "gcc и компилятор X делает это так, как и вам", потому что некоторые из этих отчетов об ошибках поступали от разработчиков крупных компаний или государственных органов, которые платили клиентам за поставщиков компиляторов. Именно по этой причине такие вещи диагностируются большинством современных компиляторов (обычно это необязательное предупреждение, которое отключено по умолчанию, например gcc -Wall) и дает поведение, которое разработчики ожидали.

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

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