Подтвердить что ты не робот

Являются ли эти совместимые типы функций в C?

Рассмотрим следующую программу C:

int f() { return 9; }
int main() {
  int (*h1)(int);
  h1 = f; // why is this allowed?                                               
  return h1(7);
}

В соответствии со стандартом C11, п. 6.5.16.1, в простом назначении, "одно из следующего будет выполнено", и единственное релевантное в списке следующее:

левый операнд имеет атомный, квалифицированный или неквалифицированный тип указателя и (учитывая тип, который будет иметь левый операнд после преобразования lvalue), оба операнда являются указателями на квалифицированные или неквалифицированные версии совместимых типов и тип, на который указывает left имеет все квалификаторы типа, на который указывает справа;

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

Мне кажется, что это ограничение нарушается при назначении в программе выше. Обе стороны назначения - указатели на функции. Итак, вопрос в том, совместимы ли два типа функций? На это дан ответ в разд. 6.7.6.3:

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

В этом случае один из типов, тип h1, имеет список типов параметров; другой, f, нет. Следовательно, последнее предложение в приведенной выше цитате применимо: в частности, "оба согласуются по числу параметров". Ясно, что h1 принимает один параметр. Как насчет f? Следующий пункт происходит непосредственно перед вышесказанным:

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

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

Однако при компиляции программы оба gcc 4.8 и Clang не выдают никаких предупреждений:

tmp$ gcc-mp-4.8 -std=c11 -Wall tmp4.c 
tmp$ cc -std=c11 -Wall tmp4.c 
tmp$

Кстати, оба компилятора выдают предупреждения, если f объявлен "int f (void)...", но это не обязательно для моего чтения стандартного выше.

Вопросы:

Q1: Назначение "h1 = f;" в программе выше нарушают ограничение "оба операнда являются указателями на квалифицированные или неквалифицированные версии совместимых типов"? В частности:

Q2: Тип h1 в выражении "h1 = f" является указателем на T1 для некоторого типа функции T1. Что такое T1?

Q3: Тип f в выражении "h1 = f" является указателем на T2 для некоторого типа функции T2. Что такое T2?

Q4: Существуют ли совместимые с T1 и T2 типы? (Пожалуйста, укажите соответствующие разделы Стандарта или других документов для поддержки ответа.)

Q1 ', Q2', Q3 ', Q4': Теперь предположим, что объявление f изменяется на "int f (void) {return 9;}". Ответьте на вопросы 1-4 снова для этой программы.

4b9b3361

Ответ 1

Эти два отчета об ошибках относятся к вашей проблеме:

Отчет о дефектах 316 говорит (акцент мой вперед):

Правила совместимости типов функций в 6.7.5.3 # 15 не определить, когда тип функции "определяется определением функциикоторый содержит (возможно, пустой) список идентификаторов", [...]

и имеет аналогичный пример с тем, который вы даете:

void f(a)int a;{}
void (*h)(int, int, int) = f;

и далее:

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

Вопрос 2: Действительно ли указанная единица перевода?

и ответ от комитета:

Комитет полагает, что ответы на Q1 и 2 даны

Это было между C99 и C11, но комитет добавил:

Мы не собираемся устанавливать старые правила стиля. Однако замечания, сделанные в этом документе, как правило, являются правильными.

и насколько я могу сказать, C99 и C11 не сильно отличаются в разделах, которые вы указали в вопросе. Если мы дополнительно рассмотрим отчет о дефектах 317, мы увидим, что он говорит:

Я считаю, что цель C - это определения функций старого стиля с пустые круглые скобки не дают функции типа, включая прототип для остальной части единицы перевода. Например:

void f(){} 
void g(){if(0)f(1);}

Вопрос 1: Определяет ли такое определение функции функцию a type включая прототип для остальной части единицы перевода?

Вопрос 2: Действительно ли указанная единица перевода?

и ответ комитетов:

Ответ на вопрос № 1 НЕТ, а на вопрос № 2 - ДА. Есть никаких нарушений ограничений, однако, если вызов функции был выполнен он будет иметь поведение undefined. См. 6.5.2.2; p6.

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

Комитет указывает, что только потому, что единица перевода действительна, это не означает, что поведение undefined отсутствует.

Ответ 2

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

Например (с использованием компилятора VS-2013):

mov         esi,esp
push        7
call        dword ptr [h1]

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

Например (с использованием компилятора VS-2013):

int f()
{
    int a = 0;
    int* p1 = &a + 4; // *p1 == 1
    int* p2 = &a + 5; // *p2 == 2
    int* p3 = &a + 6; // *p3 == 3
    return a;
}

int main()
{
    int(*h1)(int);
    h1 = f;
    return h1(1,2,3);
}

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

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

Ответ 3

Исторически, компиляторы C обычно обрабатывали передачу аргументов таким образом, чтобы гарантировать, что дополнительные аргументы будут проигнорированы, а также требуется только, чтобы программы передавали аргументы для фактически используемых параметров, что позволяло, например,

int foo(a,b) int a,b;
{
  if (a)
    printf("%d",b);
  else
    printf("Unspecified");
}

чтобы быть безопасно вызываемым через foo(1,123); или foo(0);, без необходимости указывать второй аргумент в последнем случае. Даже на платформах (например, в классическом Macintosh), чье нормальное соглашение о вызове не поддерживает такую ​​гарантию, компиляторы C обычно используют по умолчанию соглашение о вызове, которое будет поддерживать его.

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

Ответ 4

Для функций без объявленных параметров компилятор не выводит параметры/типы параметров. Следующий код по существу тот же:

int f()
{
    return 9;
}

int main()
{
    return f(7, 8, 9);
}

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

int f(void)
{
    return 9;
}

int main()
{
    return f(7, 8, 9);
}

Этот код не сможет скомпилироваться в GCC для следующей ошибки:

In function 'main':
error: too many arguments to function 'f'

Ответ 5

попробуйте использовать __stdcall перед объявлением функции - и он не будет компилироваться.
Причина в том, что вызов функции по умолчанию - __cdecl. Это означает (помимо других функций), что вызывающий объект удаляет стек после вызова. Таким образом, функция вызывающего абонента может нажимать на стек все, что он хочет, потому что он знает, что он нажал, и будет правильно очищать стек.
__stdcall означает (помимо других вещей), что вызывающая сторона очистит стек. Поэтому количество аргументов должно совпадать.
... sign говорит компилятору, что количество аргументов меняется. Если объявлено как __stdcall, то оно будет автоматически заменено на __cdecl, и вы все равно можете использовать столько аргументов, сколько хотите.

Вот почему компилятор предупреждает, но не останавливается.

Примеры
Ошибка: поврежден стек.

#include <stdio.h>

void __stdcall allmyvars(int num) {
    int *p = &num + 1;
    while (num--) {
        printf("%d ", *p);
        p++;
    }  
}

void main() {
    allmyvars(4, 1, 2, 3, 4);
}

Работа

#include <stdio.h>

void allmyvars(int num) {
    int *p = &num + 1;
    while (num--) {
        printf("%d ", *p);
        p++;
    }  
}

void main() {
    allmyvars(4, 1, 2, 3, 4);
}

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

int main() {
  int *p;
  p = (int (*)(void))f; // why is this allowed?      
  ((int (*)())p)();
  return ((int (*)())p)(7);
}

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