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

Я работаю над переносом некоторого старого кода 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;
}

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


person Carl Eric Codere    schedule 13.02.2019    source источник
comment
Какой целевой компилятор / система?   -  person dbush    schedule 13.02.2019
comment
Взгляните здесь: port70.net/~nsz/c/c11 /n1570.html#6.7.6.3 Единственный спецификатор класса хранения, который должен встречаться в объявлении параметра, - это регистр.   -  person Eugene Sh.    schedule 13.02.2019
comment
И с логической точки зрения я бы предположил, что спецификатор нужен в обоих, поскольку call генерируется на основе прототипа, а реализация основана на определении. Так что, если вызывающий будет помещать аргументы в стек, в то время как реализация может ожидать их в регистрах - у вас может быть несоответствие. Но это действительно зависит от ABI.   -  person Eugene Sh.    schedule 13.02.2019
comment
Класс хранения не является фактором при определении совместимости типов. Следовательно, тип функции, объявленной с регистром, должен быть совместим с типом функции, объявленной без регистра, если они совместимы в остальном.   -  person Eric Postpischil    schedule 13.02.2019
comment
@EricPostpischil В чем причина этого register исключения в стандарте?   -  person Eugene Sh.    schedule 13.02.2019
comment
@EugeneSh. Это не исключение. Классы хранения никогда не включаются в информацию о типе. auto int x;, static int x; и register int x; имеют тип int.   -  person PSkocik    schedule 13.02.2019
comment
@PSkocik Почему auto нельзя включить, а register можно (ну, static не имеет смысла)?   -  person Eugene Sh.    schedule 13.02.2019
comment
@EugeneSh Это интересная причуда, которую я не заметил! Я не знаю почему, но я думаю, что никто не использует auto, поэтому никто не заметил, что это также может быть безвредно разрешено в объявлении параметра.   -  person PSkocik    schedule 13.02.2019
comment
@dbush: На самом деле я работаю над модернизацией инструментария компилятора Amsterdam (ACK), чтобы он стал мультиплатформенным.   -  person Carl Eric Codere    schedule 14.02.2019
comment
Предполагая, что это пережиток того факта, что auto вообще предшествует спецификаторам типа (это было в B) и был добавлен, чтобы пометить оператор как объявление переменной; переменные, указанные в списке параметров, не нуждаются в этом, поэтому нет никакой двусмысленности, и ни один компилятор не подумал бы реализовать это. Тот факт, что позже он указывал класс хранилища, вероятно, является своего рода историческим артефактом, чтобы объяснить, почему нам нужен оператор пометки объявления сейчас, когда у нас есть имена типов в языке.   -  person Leushenko    schedule 14.02.2019


Ответы (5)


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

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

Более того, в C89 прямо говорится

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

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

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

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

ТЕМ НЕ МЕНИЕ,

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

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

person John Bollinger    schedule 13.02.2019
comment
Как было сказано выше, компилятор, с которым я работаю, совместим с ANSI C89, и в настоящее время его код - K&R, поэтому я пытаюсь немного модернизировать код ... а затем загрузить его, он использует ключевое слово register во внимание в его генераторе кода. - person Carl Eric Codere; 14.02.2019
comment
Я думаю, что, как и в случае с другими советами, приведенными ниже, беспроигрышный вариант - иметь спецификатор регистра как в определении, так и в объявлении, и это то, что я сделал, я просто хотел убедиться, что это был логичный выбор. - person Carl Eric Codere; 14.02.2019
comment
@CarlEricCodere, важно понимать, что я сказал о register. Дело не в том, что компиляторы обязательно не обращают или не должны обращать на это внимание, а скорее в том, что если они сделают, то с большей вероятностью они будут выдавать худший код, чем лучший. Компиляторы 90-х годов и позже гораздо лучше справляются с распределением данных по регистрам, чем компиляторы 70-х годов. В этом отношении им не нужна помощь программиста, поэтому обычно лучшим случаем является использование ключевого слова register излишне. - person John Bollinger; 14.02.2019
comment
Однако, если вы сохраните ключевые слова register, тогда да, лучше всего будет поместить их как в объявление, так и в определение. - person John Bollinger; 14.02.2019

Опытным путем в 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 в прототипах теоретически может изменить способ выполнения вызовов).

person PSkocik    schedule 13.02.2019
comment
Интересное наблюдение ... Я думаю, в этом случае это зависит от компилятора. - person Carl Eric Codere; 14.02.2019
comment
@CarlEricCodere Я могу ошибаться, но я не думаю, что в стандарте есть формулировка о том, как прототипы с разными классификаторами хранилища должны сочетаться с противоречащим определением, поэтому тестирование - это все, что осталось. Для квалификаторов верхнего уровня (таких как последний const int int const* const X) я думаю, что предписано, чтобы в теле использовались только те, которые указаны в определении. (Они не играют роли в типе функции.) IRC, когда я в последний раз проверял, __attributes (нестандартное расширение) и такие вещи, как _4 _ / _ 5_, имели тенденцию накапливаться. - person PSkocik; 14.02.2019

Стандарт 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);

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

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

ret 4

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

person Govind Parmar    schedule 13.02.2019
comment
Насколько я понимаю, если я не ошибаюсь, спецификатор класса хранения в параметре просто означает, что переменная, связанная с параметром, должна быть помещена в регистр (даже после запуска кода, выполняемого в подпрограмме), это не обязательно указать, что он будет вызываться с использованием соглашений о вызове регистров, нет? - person Carl Eric Codere; 14.02.2019
comment
@CarlEricCodere Стандарт фактически ничего не говорит по этой теме - только то, что ключевое слово register допустимо в списке параметров - person Govind Parmar; 14.02.2019

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

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

gcc myfile.c -S -o myfile.s
person JohnH    schedule 13.02.2019

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

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

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

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

person Frankie_C    schedule 13.02.2019