Отвечая на вопрос о распространенном неопределенном поведении в C , люди иногда ссылаются на строгое правило псевдонимов.
О чем они говорят?
805
Отвечая на вопрос о распространенном неопределенном поведении в C , люди иногда ссылаются на строгое правило псевдонимов.
О чем они говорят?
c
иc++faq
.Ответы:
Типичная ситуация, когда вы сталкиваетесь со строгими проблемами псевдонимов, - это наложение структуры (например, сообщения устройства / сети) на буфер размера слова вашей системы (например, указатель на
uint32_t
s илиuint16_t
s). Когда вы накладываете структуру на такой буфер или буфер на такую структуру с помощью приведения указателя, вы можете легко нарушить строгие правила наложения имен.Таким образом, при такой настройке, если я хочу отправить сообщение чему-либо, мне нужно иметь два несовместимых указателя, указывающих на один и тот же кусок памяти. Затем я мог бы наивно кодировать что-то вроде этого (в системе с
sizeof(int) == 2
):Строгое правило псевдонимов делает эту настройку недопустимой: разыменование указателя на псевдоним объекта, который не является совместимым типом или одним из других типов, разрешенных C 2011 6.5, пункт 7 1, является неопределенным поведением. К сожалению, вы все еще можете кодировать таким образом, возможно, получить несколько предупреждений, сделать так, чтобы он нормально компилировался, только для того, чтобы иметь странное неожиданное поведение при запуске кода.
(GCC выглядит несколько непоследовательным в своей способности давать псевдонимы предупреждениям, иногда давая нам дружеское предупреждение, а иногда нет).
Чтобы понять, почему это поведение не определено, нам нужно подумать о том, какое правило строгого алиасинга покупает компилятор. По сути, с этим правилом не нужно думать о вставке инструкций для обновления содержимого
buff
каждого запуска цикла. Вместо этого, при оптимизации с некоторыми досадно необоснованными предположениями о псевдонимах, он может пропустить эти инструкции, загрузитьbuff[0]
иbuff[1
] в регистры ЦП один раз перед запуском цикла, и ускорить тело цикла. До того, как был введен строгий псевдоним, компилятор должен был жить в состоянии паранойи, содержимое которогоbuff
может измениться в любое время и в любом месте кем-либо. Таким образом, чтобы получить дополнительное преимущество в производительности, и при условии, что большинство людей не печатают указатели, введено строгое правило псевдонимов.Имейте в виду, что если вы считаете, что пример надуманен, это может произойти, даже если вы передаете буфер другой функции, выполняющей отправку за вас, если у вас есть.
И переписал наш предыдущий цикл, чтобы воспользоваться этой удобной функцией
Компилятор может или не может быть достаточно умным, чтобы попытаться встроить SendMessage, и он может или не может решить загружать или не загружать бафф снова. Если
SendMessage
является частью другого API, который скомпилирован отдельно, он, вероятно, содержит инструкции для загрузки содержимого баффа. С другой стороны, возможно, вы находитесь в C ++, и это некая шаблонная реализация только для заголовков, которую компилятор считает, что она может быть встроенной. Или, может быть, это просто то, что вы написали в своем .c файле для вашего удобства. В любом случае неопределенное поведение все еще может возникнуть. Даже когда мы знаем о том, что происходит под капотом, это все равно является нарушением правила, поэтому не гарантируется четко определенное поведение. Так что простое включение в функцию, которая принимает наш буфер с разделителями слов, не обязательно поможет.Так как мне обойти это?
Используйте союз. Большинство компиляторов поддерживают это, не жалуясь на строгий псевдоним. Это разрешено в C99 и явно разрешено в C11.
Вы можете отключить строгое псевдонимы в вашем компиляторе ( f [no-] strict-aliasing в gcc))
Вы можете использовать
char*
для псевдонимов вместо слова вашей системы. Правила допускают исключения дляchar*
(в том числеsigned char
иunsigned char
). Всегда предполагается, чтоchar*
псевдонимы других типов. Однако это не сработает по-другому: нет предположения, что ваша структура псевдоним буфера символов.Начинающий остерегаться
Это только одно потенциальное минное поле при наложении двух типов друг на друга. Вы также должны узнать о порядке байтов , выравнивании слов и о том, как правильно решать проблемы выравнивания с помощью структур упаковки .
сноска
1 Типы, которые C 2011 6.5 7 разрешает доступ к lvalue:
источник
unsigned char*
быть использован далекоchar*
вместо? Я склонен использовать,unsigned char
а неchar
в качестве базового типа,byte
потому что мои байты не подписаны, и я не хочу, чтобы странность поведения со знаком (особенно в отношении переполнения)unsigned char *
в порядке.uint32_t* buff = malloc(sizeof(Msg));
и последующиеunsigned int asBuffer[sizeof(Msg)];
объявления буферов объединения будут иметь разные размеры, и ни один из них не будет правильным.malloc
Вызов полагается на выравнивании 4 байта под капотом (не делать) , а объединение будет в 4 раза больше , чем это должно быть ... Я понимаю , что это для ясности , но это ошибка мне ни-the меньше ...Лучшее объяснение, которое я нашел, - Майк Актон, « Понимание строгого алиасинга» . Он немного сфокусирован на разработке PS3, но в основном это только GCC.
Из статьи:
Таким образом, в основном, если у вас есть
int*
указатель на некоторую память, содержащую,int
а затем вы указываетеfloat*
на эту память и используете ее какfloat
нарушающую правило. Если ваш код не соблюдает это, оптимизатор компилятора, скорее всего, сломает ваш код.Исключением из правила является a
char*
, которому разрешено указывать на любой тип.источник
Это строгое правило псевдонимов, которое можно найти в разделе 3.10 стандарта C ++ 03 (другие ответы дают хорошее объяснение, но ни один из них не содержит самого правила):
Формулировки C ++ 11 и C ++ 14 (изменения подчеркнуты):
Два изменения были небольшими: glvalue вместо lvalue и прояснение случая совокупности / объединения.
Третье изменение дает более сильную гарантию (ослабляет строгое правило псевдонимов): новая концепция похожих типов , которые теперь безопасны для псевдонимов.
Также формулировка C (C99; ISO / IEC 9899: 1999 6.5 / 7; точно такая же формулировка используется в ISO / IEC 9899: 2011 §6.5 ¶7):
источник
wow(&u->s1,&u->s2)
он должен был бы быть законным, даже если указатель используется для измененияu
, и это отрицало бы большинство оптимизаций, которые Правило алиасинга было разработано для облегчения.Запись
Это выдержка из моего «Что такое строгое правило алиасинга и почему нас это волнует?» записать.
Что такое строгий псевдоним?
В C и C ++ псевдонимы связаны с тем, через какие типы выражений нам разрешен доступ к хранимым значениям. Как в C, так и в C ++ стандарт определяет, какие типы выражений допускаются для псевдонимов и каких типов. Компилятору и оптимизатору разрешается предполагать, что мы строго следуем правилам алиасинга, отсюда и термин строгое правило алиасинга . Если мы пытаемся получить доступ к значению с использованием недопустимого типа, оно классифицируется как неопределенное поведение ( UB ). Если у нас неопределенное поведение, все ставки отменены, результаты нашей программы перестают быть достоверными.
К сожалению, со строгими нарушениями псевдонимов мы часто получаем ожидаемые результаты, оставляя возможность того, что будущая версия компилятора с новой оптимизацией нарушит код, который мы считали действительным. Это нежелательно, и стоит понять строгие правила создания псевдонимов и избежать их нарушения.
Чтобы лучше понять, почему нас это волнует, мы обсудим проблемы, возникающие при нарушении строгих правил псевдонимов, так как типизацию наказаний часто используют, так как обычные методы, используемые при типизировании штрафов, часто нарушают строгие правила псевдонимов и как правильно вводить pun
Предварительные примеры
Давайте посмотрим на некоторые примеры, затем мы сможем поговорить о том, что конкретно говорится в стандарте (ах), рассмотрим некоторые дополнительные примеры и затем посмотрим, как избежать строгого наложения псевдонимов и выявить нарушения, которые мы пропустили. Вот пример, который не должен удивлять ( живой пример ):
У нас есть int *, указывающий на память, занятую int, и это допустимый псевдоним. Оптимизатор должен предполагать, что назначения через ip могут обновить значение, занимаемое x .
В следующем примере показан псевдоним, который приводит к неопределенному поведению ( пример в реальном времени ):
В функции foo мы берем int * и float * , в этом примере мы вызываем foo и устанавливаем оба параметра, чтобы они указывали на одну и ту же ячейку памяти, которая в этом примере содержит int . Обратите внимание, что reinterpret_cast говорит компилятору обрабатывать выражение так, как если бы оно имело тип, определенный его параметром шаблона. В этом случае мы говорим ему обрабатывать выражение & x, как если бы оно имело тип float * . Мы можем наивно ожидать, что результат второй cout будет равен 0, но при включенной оптимизации с использованием -O2 и gcc, и clang дают следующий результат:
Что может и не ожидаться, но совершенно правильно, так как мы вызвали неопределенное поведение. Число с плавающей запятой не может правильно называть объект int . Следовательно, оптимизатор может предположить, что константа 1, сохраненная при разыменовании i, будет возвращаемым значением, поскольку сохранение через f не может корректно влиять на объект int . Подсоединение кода в Compiler Explorer показывает, что это именно то, что происходит ( живой пример ):
Оптимизатор, использующий анализ псевдонимов на основе типов (TBAA), предполагает, что 1 будет возвращен, и непосредственно перемещает постоянное значение в регистр eax, который несет возвращаемое значение. TBAA использует правила языков о том, какие типы разрешены для псевдонимов для оптимизации загрузки и хранения. В этом случае TBAA знает, что float не может использовать псевдонимы и int, и оптимизирует загрузку i .
Теперь к книге правил
Что именно стандарт говорит, что нам разрешено и не разрешено делать? Стандартный язык не является простым, поэтому для каждого элемента я постараюсь предоставить примеры кода, которые демонстрируют значение.
Что говорит стандарт C11?
Стандарт C11 говорит следующее в разделе 6.5 Выражения параграфа 7 :
gcc / clang имеет расширение, а также позволяет присваивать int без знака int * значение int *, даже если они несовместимы.
Что говорит C ++ 17 Draft Standard
Проект стандарта C ++ 17 в разделе 11 [basic.lval] гласит:
Стоит отметить, что подписанный символ не включен в приведенный выше список, это заметное отличие от C, который говорит о типе символа .
Что такое Type Punning
Мы дошли до этой точки, и нам может быть интересно, зачем нам нужен псевдоним? Ответ обычно заключается в вводе слов , часто используемые методы нарушают строгие правила наложения имен.
Иногда мы хотим обойти систему типов и интерпретировать объект как другой тип. Это называется типом паннинга , чтобы переосмыслить сегмент памяти как другой тип. Тип punning полезен для задач, которые хотят получить доступ к базовому представлению объекта для просмотра, транспортировки или манипулирования. Типичные области, в которых мы находим использование типов ввода: компиляторы, сериализация, сетевой код и т. Д.
Традиционно это было достигнуто путем взятия адреса объекта, приведения его к указателю типа, который мы хотим переинтерпретировать как, и последующего доступа к значению, или другими словами, с помощью псевдонимов. Например:
Как мы видели ранее, это неверный псевдоним, поэтому мы вызываем неопределенное поведение. Но традиционно компиляторы не пользовались преимуществами строгих правил псевдонимов, и этот тип кода обычно просто работал, разработчики, к сожалению, привыкли делать такие вещи. Распространенный альтернативный метод для обозначения типов - через объединения, что допустимо в C, но неопределенное поведение в C ++ ( см. Живой пример ):
Это недопустимо в C ++, и некоторые считают, что объединение предназначено исключительно для реализации типов вариантов, и считают, что использование объединений для наказания типов является злоупотреблением.
Как правильно печатать Pun?
Стандартный метод для определения типов в C и C ++ - это memcpy . Это может показаться немного сложным, но оптимизатор должен распознавать использование memcpy для обозначения типа, оптимизировать его и генерировать регистр для регистрации перемещения. Например, если мы знаем, что int64_t имеет тот же размер, что и double :
мы можем использовать memcpy :
При достаточном уровне оптимизации любой приличный современный компилятор генерирует код, идентичный ранее упомянутому методу reinterpret_cast или методу объединения для определения типов . Изучая сгенерированный код, мы видим, что он использует только регистр mov ( живой пример Compiler Explorer ).
C ++ 20 и bit_cast
В C ++ 20 мы можем получить bit_cast ( реализация доступна по ссылке в предложении ), который дает простой и безопасный способ ввода слов, а также может использоваться в контексте constexpr.
Ниже приведен пример того, как использовать bit_cast для ввода pun беззнакового целого типа с плавающей точкой ( смотрите в реальном времени ):
В случае, когда типы To и From не имеют одинаковый размер, это требует от нас использования промежуточной структуры15. Мы будем использовать структуру, содержащую символьный массив sizeof (unsigned int) ( предполагается, что 4-байтовое unsigned int ) будет типом From, а unsigned int - типом To . :
К сожалению, нам нужен этот промежуточный тип, но это текущее ограничение bit_cast .
Ловить строгие алиасинговые нарушения
У нас не так много хороших инструментов для отслеживания строгого псевдонима в C ++, инструменты, которые у нас есть, будут отлавливать некоторые случаи строгих нарушений псевдонимов и некоторые случаи неправильной загрузки и хранения.
gcc с использованием флагов -fstrict-aliasing и -Wstrict-aliasing может отлавливать некоторые случаи, хотя и без ложных срабатываний / отрицаний. Например, в следующих случаях в gcc будет сгенерировано предупреждение ( смотрите его вживую ):
хотя он не поймает этот дополнительный случай ( посмотри вживую ):
Хотя clang разрешает эти флаги, он, по-видимому, фактически не реализует предупреждения.
Еще один инструмент, который у нас есть, - это ASan, который может улавливать смещенные грузы и запасы. Хотя это не является прямым строгим нарушением псевдонимов, это общий результат строгих нарушений псевдонимов. Например, в следующих случаях будут генерироваться ошибки времени выполнения при сборке с помощью clang с использованием -fsanitize = address
Последний инструмент, который я порекомендую, специфичен для C ++ и не только инструмент, но и практика кодирования, не допускающая приведение в стиле C. И gcc, и clang будут производить диагностику для приведения в стиле C с использованием -Wold-style-cast . Это заставит любые неопределенные каламбуры типа использовать reinterpret_cast, в общем случае reinterpret_cast должен быть флагом для более тщательного анализа кода. Также проще выполнить поиск в базе кода для reinterpret_cast, чтобы выполнить аудит.
Для C у нас есть все инструменты, которые уже были рассмотрены, и у нас также есть TIS-интерпретатор, статический анализатор, который исчерпывающе анализирует программу для большого подмножества языка C. Учитывая C-версии предыдущего примера, где использование -fstrict-aliasing пропускает один случай ( смотрите его вживую )
tis-interpeter способен перехватить все три, в следующем примере tis-kernal вызывается как tis-интерпретатор (выходные данные редактируются для краткости):
Наконец, TySan, который в настоящее время находится в разработке. Это дезинфицирующее средство добавляет информацию о проверке типов в сегменте теневой памяти и проверяет доступы, чтобы определить, не нарушают ли они правила псевдонимов. Инструмент потенциально должен быть в состоянии отследить все нарушения псевдонимов, но может иметь большие накладные расходы во время выполнения.
источник
reinterpret_cast
может сделать или чтоcout
может означать. (Можно упомянуть C ++, но первоначальный вопрос был о C и IIUC, эти примеры можно было бы так же правильно написать на C.)Строгое псевдонимы относятся не только к указателям, но и к ссылкам, я написал статью об этом для вики для продвинутых разработчиков, и он был настолько хорошо принят, что я превратил его в страницу на своем консультационном веб-сайте. Это полностью объясняет, что это такое, почему это так сильно смущает людей и что с этим делать. Строгий Aliasing White Paper . В частности, это объясняет, почему объединения являются рискованным поведением для C ++, и почему использование memcpy является единственным переносимым исправлением как для C, так и для C ++. Надеюсь, это полезно.
источник
Как дополнение к тому, что уже написал Дуг Т., вот простой тестовый пример, который, вероятно, запускает его с помощью gcc:
check.c
Компилировать с
gcc -O2 -o check check.c
. Обычно (с большинством версий gcc, которые я пробовал) это выдает «проблему строгого алиасинга», потому что компилятор предполагает, что «h» не может быть тем же адресом, что и «k» в функции «check». Из-за этого компилятор оптимизируетif (*h == 5)
компоновку и всегда вызывает printf.Для тех, кого это интересует, код ассемблера x64, созданный gcc 4.6.3, работает на ubuntu 12.04.2 для x64:
Таким образом, условие if полностью ушло из ассемблерного кода.
источник
long long*
иint64_t
*). Можно было бы ожидать, что здравомыслящий компилятор должен признать, чтоlong long*
иint64_t*
может получить доступ к одному и тому же хранилищу, если они хранятся одинаково, но такое обращение уже не модно.Выделение типов с помощью приведения указателей (в отличие от использования объединения) является основным примером нарушения строгого алиасинга.
источник
fpsync()
директиву между записью в виде fp и чтением в виде int или наоборот [в реализациях с отдельными целочисленными и конвейерами и кэшами FPU такая директива может быть дорогой, но не такой дорогой, как компилятор, выполняющий такую синхронизацию при каждом доступе к объединению]. Или реализация может указывать, что полученное значение никогда не будет пригодным для использования, за исключением случаев, когда используются общие начальные последовательности.Согласно обоснованию C89, авторы стандарта не хотели требовать, чтобы компиляторы давали такой код:
должно потребоваться перезагрузить значение
x
между оператором присваивания и возврата, чтобы учесть возможность, на которую онp
может указыватьx
, и присваивание, которое*p
может впоследствии изменить значениеx
. Идея о том, что компилятор должен иметь право предполагать, что в ситуациях, подобных описанным выше, не будет псевдонимов, не вызывает сомнений.К сожалению, авторы C89 написали свое правило таким образом, что, если читать буквально, заставит даже следующую функцию вызывать Undefined Behavior:
потому что он использует lvalue типа
int
для доступа к объекту типаstruct S
, иint
не входит в число типов, которые могут использоваться для доступа кstruct S
. Поскольку было бы абсурдно рассматривать любое использование элементов структур и объединений, не относящихся к символьному типу, как неопределенное поведение, почти каждый признает, что существуют, по крайней мере, некоторые обстоятельства, когда lvalue одного типа может использоваться для доступа к объекту другого типа. , К сожалению, Комитет по стандартам C не смог определить, каковы эти обстоятельства.Большая часть проблемы связана с отчетом о дефектах № 028, в котором задан вопрос о поведении такой программы, как:
В отчете о дефектах № 28 говорится, что программа вызывает неопределенное поведение, потому что действие записи члена объединения типа «double» и чтения одного типа «int» вызывает поведение, определяемое реализацией. Такие рассуждения бессмысленны, но формируют основу для правил эффективного типа, которые излишне усложняют язык, не делая ничего для решения исходной проблемы.
Вероятно, наилучшим способом решения исходной проблемы было бы рассматривать сноску о цели правила, как если бы она была нормативной, и сделать правило неосуществимым, за исключением случаев, когда на самом деле возникают конфликты при доступе с использованием псевдонимов. Учитывая что-то вроде:
Внутри нет конфликта,
inc_int
потому что все обращения к хранилищу, через которое*p
осуществляется доступ , выполняются с lvalue типаint
, и здесь нет конфликта,test
потому чтоp
он визуально получен изstruct S
, и при следующемs
использовании все обращения к этому хранилищу, которые когда-либо будут сделаны черезp
уже произошло.Если код был изменен немного ...
Здесь существует конфликт псевдонимов между
p
и доступом кs.x
отмеченной строке, потому что в этот момент выполнения существует другая ссылка, которая будет использоваться для доступа к тому же хранилищу .Если бы в отчете о дефектах 028 говорилось, что исходный пример вызвал UB из-за совпадения между созданием и использованием двух указателей, это сделало бы вещи более ясными без добавления «эффективных типов» или других подобных сложностей.
источник
Прочитав многие ответы, я чувствую необходимость что-то добавить:
Строгий псевдоним (который я опишу чуть позже) важен, потому что :
Доступ к памяти может быть дорогим (с точки зрения производительности), поэтому данные обрабатываются в регистрах ЦП, прежде чем они записываются обратно в физическую память.
Если данные в двух разных регистрах ЦП будут записаны в одно и то же пространство памяти, мы не можем предсказать, какие данные «выживут», когда мы кодируем в C.
В сборке, где мы кодируем загрузку и выгрузку регистров ЦП вручную, мы узнаем, какие данные остаются нетронутыми. Но C (к счастью) абстрагируется от этой детали.
Поскольку два указателя могут указывать на одно и то же место в памяти, это может привести к сложному коду, который обрабатывает возможные коллизии .
Этот дополнительный код медленен и снижает производительность, поскольку он выполняет операции чтения / записи дополнительной памяти, которые являются одновременно более медленными и (возможно) ненужными.
Строгое правило сглаживания позволяет избежать избыточного кода машины в тех случаях , в которых он должен быть с уверенностью предположить , что два указателя не указывают на тот же блок памяти (смотри также
restrict
ключевое слово).Строгий псевдоним утверждает, что можно предположить, что указатели на разные типы указывают на разные места в памяти.
Если компилятор заметит, что два указателя указывают на разные типы (например, a
int *
и afloat *
), он будет считать, что адрес памяти отличается, и он не защитит от конфликтов адресов памяти, что приведет к более быстрому машинному коду.Например :
Давайте возьмем следующую функцию:
Чтобы обработать случай, когда
a == b
(оба указателя указывают на одну и ту же память), нам нужно упорядочить и протестировать способ загрузки данных из памяти в регистры ЦП, чтобы код мог выглядеть примерно так:загрузить
a
иb
из памяти.добавить
a
кb
.сохранить
b
и перезагрузитьa
.(сохранить из регистра ЦП в память и загрузить из памяти в регистр ЦП).
добавить
b
кa
.сохранить
a
(из регистра ЦП) в память.Шаг 3 очень медленный, потому что ему нужен доступ к физической памяти. Тем не менее, это необходимо для защиты от случаев, когда
a
иb
указывают на тот же адрес памяти.Строгий псевдоним позволит нам предотвратить это, сообщив компилятору о том, что эти адреса памяти явно различаются (что в этом случае позволит даже дальнейшую оптимизацию, которая не может быть выполнена, если указатели совместно используют адрес памяти).
Об этом можно сказать компилятору двумя способами, используя разные типы для указания. то есть:
Используя
restrict
ключевое слово. то есть:Теперь, соблюдая правило строгого псевдонима, можно избежать шага 3, и код будет работать значительно быстрее.
Фактически, добавив
restrict
ключевое слово, можно оптимизировать всю функцию:загрузить
a
иb
из памяти.добавить
a
кb
.сохранить результат как до, так
a
и доb
.Эта оптимизация не могла быть сделана раньше из-за возможного столкновения (где
a
иb
было бы утроено, а не удвоено).источник
b
(не перезагружаем) и перезагружаемa
. Надеюсь, теперь стало понятнее.restrict
, но я думаю, что последний в большинстве случаев будет более эффективным, а ослабление некоторых ограниченийregister
позволит ему заполнить некоторые случаи, гдеrestrict
это не поможет. Я не уверен, что когда-либо было «важно» рассматривать Стандарт как полностью описывающий все случаи, когда программисты должны ожидать, что компиляторы будут распознавать свидетельства псевдонимов, а не просто описывать места, где компиляторы должны предполагать псевдонимы, даже когда нет конкретных доказательств его существования .restrict
ключевое слово минимизирует не только скорость операций, но и их количество, что может быть значимым ... Я имею в виду, в конце концов, самая быстрая операция - вообще не операция :)Строгий псевдоним не позволяет использовать разные типы указателей для одних и тех же данных.
Эта статья должна помочь вам понять проблему в деталях.
источник
int
структура, которая содержитint
).Технически в C ++ строгое правило псевдонимов, вероятно, никогда не применимо.
Обратите внимание на определение косвенности ( оператор * ):
Также из определения glvalue
Таким образом, в любой четко определенной программной трассировке glvalue ссылается на объект. Так что так называемое правило строгого наложения не применяется никогда. Возможно, это не то, что хотели дизайнеры.
источник
int foo;
, к чему обращается выражение lvalue*(char*)&foo
? Это объект типаchar
? Этот объект появляется одновременноfoo
? Будет ли запись дляfoo
изменения сохраненного значения вышеупомянутого объекта типаchar
? Если так, есть ли какое-либо правило, которое позволилоchar
бы получить доступ к сохраненному значению объекта типа , используя lvalue типаint
?int i;
четыре объекта каждого символьного типаin addition to one of type
int? I see no way to apply a consistent definition of "object" which would allow for operations on both
* (char *) & i` иi
. Наконец, в Стандарте нет ничего, что позволяло бы дажеvolatile
квалифицированному указателю получать доступ к аппаратным регистрам, которые не соответствуют определению «объекта».