Совместимость указателей функций между одиночным указателем и пустыми списками параметров

Я читал о совместимости указателей функций, но не нашел следующий сценарий, задокументированный как приемлемый (ниже).

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

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

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

Программа

#include <stdlib.h>
#include <stdio.h>

void functionA()
{
    printf("A\n");
}

void functionB(uint8_t* parameter)
{
    printf("B %d\n", *parameter);
}

void (*functionPointer)();

int main()
{
    uint8_t number = 42;

    functionPointer = functionA;
    functionPointer(&number);

    functionPointer = functionB;
    functionPointer(&number);
    
    //functionA(&number); // warning: too many arguments in call

    return 0;
}

Выход

% ./a.out
A
B 42

Семантика пустых круглых скобок изменилась между C99 и C23. В C23 это означает отсутствие аргументов (т. е. void fun() то же самое, что void fun(void)). До этого это означало, что аргументы не указаны, и аргументов может быть любое количество и любой тип.

Some programmer dude 22.03.2024 11:53

Интересно, можно ли сказать, что этот код строго соответствует стандартам C до C23? Я отредактировал приведенную выше программу, чтобы добавить комментарий к вызову functionA(&number);, что приводит к «Предупреждение: слишком много аргументов в вызове функции A». Получается, в этом сценарии это действует только через указатель на функцию?

zambetti 22.03.2024 12:00

До C23 вызов functionPointer(&number);, когда functionPointer указывает на functionA, является UB, поскольку functionPointer не включает прототип и количество аргументов не равно количеству параметров functionA, но вызов functionA(&number); является нарушением ограничения. В C23 и functionA(&number);, и functionPointer(&number); (когда functionPointer указывает на functionA) являются UB из-за несоответствия аргумента/параметра.

Ian Abbott 22.03.2024 13:18
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
3
119
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Объявление функции, включающее тип ее параметров, называется прототипом (C 2018 6.2.1 2).

Ожидается, что правила, касающиеся типов функций без объявлений параметров, изменятся в C 2023. Ожидается, что C 2023 удалит из стандарта объявления, не являющиеся прототипами, и сделает () в объявлении функции эквивалентным (void), поэтому код в вопросе будет не-прототипом. соответствие, что делает задаваемые вопросы в значительной степени спорными.

Остальная часть этого ответа касается C 2018, который в этом отношении практически не изменился по сравнению с C 1999.

Я читал о совместимости указателей функций, но не нашел следующий сценарий, задокументированный как приемлемый (ниже).

Вызов функции, которая определена с прототипом, с использованием выражения, не имеющего прототипа, указан в C 2018 6.5.2.2 6. («Здесь используется выражение», а не просто ссылка на имя функции, поскольку функцию можно вызвать с помощью имя функции, переменная, которая является указателем на тип функции, или выражение, которое представляет собой приведение, приводящее к указателю на тип функции.) Вызов функции, которая не определена с прототипом, с использованием выражения, у которого есть прототип. указано в следующих пунктах.

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

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

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

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

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

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

При этом в определениях функций используется .... В этом случае компилятор вообще не может знать аргументы. Более того, аргументы не обязательно известны вызывающей программе. Хотя printf, например, имеет строку формата, которая сообщает ему об ожидаемых аргументах, допустимо передавать printf больше аргументов, чем требует строка форматирования (C 2018 7.21.6.1 2). На платформе, где за очистку стека отвечает вызывающая сторона, это не проблема, поскольку вызывающая сторона знает, какие аргументы были переданы. На платформе, где вызывающая функция отвечает за очистку стека, это может быть реализовано с помощью правил интерфейса, которые требуют, чтобы вызывающая подпрограмма передавала информацию об аргументах. Это будет «скрытая» информация, которая не отображается в аргументах. Например, интерфейс может потребовать, чтобы вызывающая подпрограмма передала количество байтов, которые необходимо удалить из стека при возврате из вызова функции.

Приведенный ниже код компилируется и запускается без предупреждений, потому что он на самом деле является допустимым C?

Точнее, он может компилироваться и выполняться без предупреждений, поскольку он соответствует коду C. («Допустимый» имеет другое значение и не определен в стандарте C. Он допустим для программ, чтобы использовать расширения языка C.) Однако это не является строго соответствующим коду C. Поскольку первый вызов с использованием functionPointer передает аргумент функции, определенной без параметров, это нарушает C 2018 6.5.2.2 6: «… Если количество аргументов не равно количеству параметров, поведение не определено…» Это делает поведение, не определенное стандартом C.

Обратите внимание на разницу между функциями, определенными с ... и без него. Хотя мы можем передавать printf аргументы, о которых он не знает, стандарт не гарантирует, что мы сможем сделать это для functionA. Причина в том, что ... предупреждает компилятор о том, что он должен, если этого требует платформа, использовать дополнительную информацию об аргументах, описанную выше. Без ... компилятор может сгенерировать код, ожидаемый с фиксированным количеством аргументов и фиксированными типами, и поэтому это будет неправильно при вызове с другим количеством аргументов.

Действительно ли эти типы функций совместимы, и если да, то определено ли это поведение?

Да. Правила совместимости типов функций указаны в C 2018 6.7.6.3 15 и позволяют прототипным типам быть совместимыми с непрототипными типами. Для типов в C «совместимость» в основном означает, что два типа могут быть дополнены одним и тем же типом, т. е. что любые их указанные части являются одинаковыми. Например, тип массива с размером (количество элементов в массиве) совместим с типом массива без размера (число не указано). Аналогично, два типа функций, которые имеют один и тот же тип возвращаемого значения, но отличаются тем, что один определяет типы параметров, а другой несовместимы.

Спасибо за такое удивительно подробное объяснение и за ответы на все мои вопросы. Учитывая изменения в C 2023, вероятно, будет лучше, если я буду избегать передачи параметров в функции без объявлений параметров, вызываемых через указатель.

zambetti 23.03.2024 13:38

В C17 и более ранних версиях functionPointer определяется как указатель на функцию, принимающую неопределенное количество аргументов и возвращающую void. Такой указатель на функцию совместим с любой функцией, возвращающей void.

Стандарт C11 дает следующее описание определения совместимости двух типов функций, раздел 6.7.6.3p15:

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

Здесь применимы разделы, выделенные жирным шрифтом. Таким образом, присвоение functionA или functionBfunctionPointer четко определено.

Однако проблема в том, как functionA вызывается через этот указатель:

functionPointer = functionA;
functionPointer(&number);

В разделе 6.5.2.2p2 указано следующее ограничение на вызовы функций:

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

Поскольку приведенный выше вызов functionA с неправильным количеством параметров, это нарушение ограничений, и поэтому код имеет неопределенное поведение.

Начиная с C23, тип functionPointer является указателем на функцию, которая не принимает параметров и возвращает void. Это указано в разделе 6.7.6.3p13 стандарта C23

Для декларатора функции без списка типов параметров: эффект как если бы он был объявлен со списком типов параметров, состоящим из ключевое слово void. Декларатор функции предоставляет прототип для функция

Таким образом, functionPointer = functionA, скорее всего, выдаст предупреждение о несовместимом преобразовании указателя, а functionPointer(&number) выдаст ошибку при вызове функции с неправильным количеством параметров.

Другие вопросы по теме