Должен ли я поместить спецификатор класса хранения параметров в определение функции или в объявление и определение?

Я работаю над переносом старого кода K & R на ANSI C, поэтому пишу отсутствующие объявления прототипов функций. У многих определений функций есть параметры с классом хранения регистров, но я не уверен, можно ли опустить спецификатор класса хранения регистров в прототипе функции?

С определением класса хранилища регистров и без него код компилируется правильно (я пробовал GCC, VC++ и Watcom C). Я не смог найти никакой информации в стандарте ISO/ANSI C89 о том, что делать правильно - нормально ли, если я просто введу ключевое слово register в определение функции?

int add(register int x, register int y); 

int add(register int x, register int y)
{
  return x+y;
}

Это также строит правильно:

int add(int x, int y);

int add(register int x, register int y)
{
   return x+y;
}

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

Ответ 1

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

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

Более того, C89 специально говорит

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

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

С и без специального объявления класса хранилища регистров код компилируется правильно (я пробовал gcc, VC++ и Watcom), я не смог найти никакой информации в стандарте ISO/ANSI C89 о том, что делать правильно, или это нормально, если я просто поместил ключевое слово register в определении функции?

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

ТЕМ НЕ МЕНИЕ,

  1. Ключевое слово register является реликвией. Компиляторы не обязаны делать какие-либо попытки фактически назначить register переменные регистрам, и современные компиляторы намного лучше, чем люди, решают, как назначать переменные в регистры и каким-либо другим образом генерировать быстрый код. Пока вы конвертируете старый код, я бы воспользовался возможностью удалить все появления ключевого слова register.

  2. С89 устарел. Последняя версия стандарта - C 2018; C 2011 широко развернут; и C99 (также, технически, устаревший) доступен почти везде. Возможно, у вас есть веская причина для таргетинга на C89, но вам следует строго рассмотреть нацеленность на C11 или C18, или, по крайней мере, на C99.

Ответ 2

Стандарт C89 действительно говорит это (§ 3.5.4.3 Внешние определения):

Единственный спецификатор класса хранения, который должен присутствовать в объявлении параметра, это register.

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

Поскольку вы упомянули Watcom и C89, я предполагаю, что вы ориентируетесь на x86-16. Типичные соглашения о вызове для x86-16 (pascal, stdcall и cdecl) все требуют параметры, которые будут в стек, а не в регистрах, поэтому я сомневаюсь, что ключевое слово будет на самом деле изменить как параметры передаются функции при вызове сайт.

Предположим, у вас есть следующее определение функции:

int __stdcall add2(register int x, register int y);

Функция переходит в объектный файл как [email protected] согласно требованиям для stdcall. @4 указывает, сколько байтов нужно удалить из стека при возврате функции. В этом случае используется ret imm16 (возврат к вызывающей процедуре и извлечение байтов imm16 из стека).

add2 будет иметь следующий ret в конце:

ret 4

Если 4 байта не были помещены в стек на месте вызова (то есть, потому что параметры были фактически в регистрах), ваша программа теперь имеет неправильно выровненный стек и вылетает.

Ответ 3

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

(что касается квалификаторов верхнего уровня, они также отбрасываются, когда рассматривается совместимость типов, т.е. void f(int); и void f(int const); являются совместимыми прототипами, но классы хранения не являются частью типов, поэтому совместимость типов не проблема с ними в первую очередь)

С точки зрения программиста на C, единственным наблюдаемым результатом register в C является то, что компилятор не позволит вам взять адрес объявленного объекта.

Когда я делаю:

void f(int A, int register B);

void f(int register A, int B) 
{
    /*&A;*/ //doesn't compile => A does have register storage here
    &B; //compiles => B doesn't have register storage here;
        //the register from the previous prototype wasn't considered
}

затем &B компилируется, а &A нет, поэтому подсчитываются только квалификаторы в определении.

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

Ответ 4

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

Это означает, что вы хотите запускать каждый вариант вашего примера через компилятор, с параметром компилятора, установленным для генерации сборки вместо исполняемого файла. Посмотрите на сборку и посмотрите, сможете ли вы определить, использует ли она регистры или нет. В gcc это опция S; например:

gcc myfile.c -S -o myfile.s

Ответ 5

От стандарта C17, 6.7.1 Спецификаторы класса хранения:

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

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

Таким образом, оно должно присутствовать в определении функции, но незначительно в прототипе.