Что говорит стандарт C
Обратите внимание, что функции variadic могут вызываться только при наличии прототипа. Если вы попытаетесь вызвать printf()
без прототипа, вы получите UB (неопределенное поведение).
C11 §6.5.2.2 Вызов функции ¶6 говорит:
If6 Если выражение, обозначающее вызываемую функцию, имеет тип, который не содержит прототип, целочисленные преобразования выполняются для каждого аргумента, а аргументы, имеющие тип float
, переводятся в double
. Они называются продвижениями аргументов по умолчанию. Если число аргументов не равно количеству параметров, поведение не определено. Если функция определена с типом, который включает в себя прототип, и либо прототип заканчивается многоточием (, ...
), либо типы аргументов после продвижения не совместимы с типами параметров, поведение не определено. Если функция определена с типом, который не содержит прототип, и типы аргументов после продвижения не совместимы с типами параметров после продвижения, поведение не определено, за исключением следующих случаев:
- один повышенный тип является целочисленным типом со знаком, другой повышенный тип является целочисленным типом без знака, и значение может быть представлено в обоих типах;
- оба типа являются указателями на квалифицированные или неквалифицированные версии типа символа или
void
.
Применительно к исходному коду в вопросе
Исходный код в вопросе был похож на это - последовательные идентичные вызовы функций были уменьшены до одного вызова.
void void_unspec(), void_void(void);
void call_void_void()
{
void_void();
}
void call_void_unspec()
{
void_unspec();
void_unspec(.0,.0,.0);
void_unspec(.0,.0,.0,.0,.0,.0,.0,.0);
void_unspec(.0,.0,.0,.0,.0,.0,.0,.0,.0,.0);
}
Этот код вызывает UB, потому что количество аргументов для вызовов функции к void_unspec()
не все совпадают с числом аргументов, которые он определен принять (независимо от того, что это определение; он не может одновременно принимать 0, 3, 8 и 10 аргументов). Это не нарушение ограничений, поэтому диагностика не требуется. Компилятор обычно делает все, что считает наилучшим для обратной совместимости, и обычно не вызывает явных сбоев, но любые проблемы, которые возникают у программиста за нарушение правил стандарта.
И поскольку стандарт говорит, что поведение не определено, нет конкретной причины, по которой компилятор должен устанавливать %rax
(конечно, стандарт C ничего не знает о %rax
), но простая согласованность предполагает, что он должен .
Применительно к пересмотренному коду в вопросе
Код в вопросе был изменен следующим образом (повторные последовательные вызовы снова пропущены):
void void_unspec0(), void_unspec1(), void_unspec2(), void_unspec3(), void_void(void);
void call_void_void()
{
void_void();
}
void call_void_unspec()
{
void_unspec0();
void_unspec1(.0,.0,.0);
void_unspec2(.0,.0,.0,.0,.0,.0,.0,.0);
void_unspec3(.0,.0,.0,.0,.0,.0,.0,.0,.0,.0);
}
Код больше не неизбежно вызывает неопределенное поведение. Однако там, где определены функции void_unspec0()
и т.д., они должны выглядеть примерно так:
void void_unspec0(void) { … }
void void_unspec1(double a, double b, double c) { … }
void void_unspec2(double a, double b, double c, double d, double e, double f, double g, double h) { … }
void void_unspec3(double a, double b, double c, double d, double e, double f, double g, double h, double i, double j) { … }
Одна эквивалентная запись будет:
void void_unspec2(a, b, c, d, e, f, g, h)
double a, b, c, d, e, f, g, h;
{
…
}
Используется нестандартное определение K & R, не являющееся прототипом.
Если определения функций не соответствуют этим, то в §6.5.2.2¶6 говорится, что результатом вызовов является неопределенное поведение. Это избавляет стандарт от необходимости законодательно определять, что происходит при всевозможных сомнительных обстоятельствах. Как и прежде, компилятор может передавать число значений с плавающей запятой в %rax
; в этом есть смысл. Но очень мало что можно сделать, чтобы спорить о том, что произойдет - либо вызовы соответствуют определению, и все в порядке, либо нет, и есть неуказанные (и неуказанные) потенциальные проблемы.
Обратите внимание, что ни call_void_void()
, ни call_void_unspec()
не определены с прототипом. Обе эти функции принимают нулевые аргументы, но нет видимого прототипа, который обеспечивал бы это, поэтому код в том же файле мог вызвать call_void_void(1, "abc")
без жалоб компилятора. (В этом отношении, как и во многих других, C ++ - это другой язык с другими правилами.)