У меня проблема с пониманием того, что можно, а что нельзя делать с помощью объединений с GCC. Я прочитал вопросы (в частности, здесь и здесь), но они фокусируются на стандарте C ++, я чувствую несоответствие между стандартом C ++ и практикой (обычно используемыми компиляторами).
В частности, недавно я обнаружил сбивающую с толку информацию в онлайн-документе GCC при чтении о флаге компиляции -fstrict-aliasing. Он говорит:
-fstrict-aliasing
Позвольте компилятору принять самые строгие правила псевдонима, применимые к компилируемому языку. Для C (и C ++) это активирует оптимизацию в зависимости от типа выражений. В частности, предполагается, что объект одного типа никогда не находится по тому же адресу, что и объект другого типа, если только типы почти не совпадают. Например,
unsigned int
может быть псевдонимомint
, но неvoid*
илиdouble
. Тип символа может быть псевдонимом любого другого типа. Обратите особое внимание на такой код:union a_union { int i; double d; }; int f() { union a_union t; t.d = 3.0; return t.i; }
Распространена практика чтения от другого члена профсоюза, чем тот, которому в последний раз писали (так называемая «каламбур»). Даже с -fstrict-aliasing разрешено использование символов при условии, что доступ к памяти осуществляется через тип объединения. Итак, приведенный выше код работает так, как ожидалось.
Вот что я понял из этого примера и своих сомнений:
1) псевдонимы работают только между похожими типами или char
Следствие 1): алиасинг — как следует из этого слова — это когда у вас есть одно значение и два члена для доступа к нему (то есть одни и те же байты);
Сомнение: ли два типа похожи, если они имеют одинаковый размер в байтах? Если нет, то какие бывают похожие типы?
Следствие 1) для несхожих типов (что бы это ни значило), алиасинг не работает;
2) каламбур — это когда мы читаем член, отличный от того, которому мы писали; это обычное дело, и оно работает должным образом, пока доступ к памяти осуществляется через тип объединения;
Сомнение: псевдоним — это конкретный случай каламбура, когда типы похожи?
Я запутался, потому что там говорится, что unsigned int и double не похожи, поэтому сглаживание не работает; затем в примере это псевдоним между int и double, и он четко говорит, что работает так, как ожидалось, но называет это каламбуром: не потому, что типы похожи или не похожи, а потому, что он читает из члена, который не записывал. Но я понял, что псевдонимы предназначены для чтения от члена, которого он не писал (как следует из этого слова). Я потерялся.
Вопросы: может ли кто-нибудь прояснить разницу между псевдонимом и каламбуром, и какое использование этих двух методов работает должным образом в GCC? А что делает флаг компилятора?
Я чувствую несоответствие между спецификациями и практикой, пока вы не обновите свой компилятор и все не нанесет ущерб! (правдивая история) — person L.C. schedule 19.02.2019
Когда вам действительно нужно набирать текст: stackoverflow.com/a/17790026/8120642 — person L.C. schedule 19.02.2019
Псевдонимы можно понимать буквально: это когда два разных выражения относятся к одному и тому же объекту. Каламбур — это «каламбур» типа, то есть использование объекта одного типа в качестве другого типа.
Формально, каламбур — это неопределенное поведение, за редким исключением. Обычно это случается, когда вы небрежно возитесь с битами
Исключения (упрощенные)
char
,unsigned char
илиstd::byte
Это известно как правило строгого псевдонима: компилятор может безопасно предполагать, что два выражения разных типов никогда не ссылаются на один и тот же объект (за исключением исключений, указанных выше), потому что в противном случае они имели бы неопределенное поведение. Это облегчает такую оптимизацию, как
Что говорит gcc, так это то, что он немного смягчает правила и позволяет набирать типы через объединения, даже если стандарт не требует, чтобы
Это тип-каламбур, который гарантирует gcc. Другие корпуса могут показаться работоспособными, но однажды они могут быть тихо сломаны.
Я думаю, что ваш пример не работает в том смысле, что расположение битовых полей в этой структуре само определяется реализацией. Плохое определение битовых полей в C — одна из тех действительно раздражающих вещей, которые, вероятно, уже слишком поздно исправить. Типа каламбур подходит (по крайней мере, в GCC), но битовое поле может делать то, что вы ожидаете, а может и не делать. — person L.C.; 19.02.2019
@DanMills Fair, но я не мог придумать красивый и легкий каламбур из головы. Я подумал, что если я хочу показать то, что практически работает, то могу пойти до конца. — person L.C.; 19.02.2019
@Passer Один довольно распространенный пример — это что-то вроде
union { long long x; struct { unsigned low, high } }
(или то же самое, но сunsigned[2]
, вы уловили идею). — person L.C.; 19.02.2019В контекстах, отличных от интерпретации gcc / clang правила строгого псевдонима, термин псевдоним не будет использоваться для описания ситуаций, в которых одна ссылка используется для получения другой, а новая ссылка используется для доступа к объекту, а затем отбрасывается до объект используется любым другим способом. — person L.C.; 19.02.2019
@supercat Я не совсем понимаю вашу точку зрения. Псевдоним — это не интерпретируемое значение, это английское слово. Строгий псевдоним как фраза не существует в стандарте C ++, но обычно относится к [expr.lval]. — person L.C.; 20.02.2019
@PasserBy: тот факт, что две ссылки идентифицируют один и тот же объект в непересекающиеся моменты времени, не подразумевает псевдонима. Также не используется тот факт, что ссылка используется для получения другой ссылки, которая немедленно используется для доступа к объектам. Это оба ожидаемых шаблонов доступа, в то время как термин «псевдонимы» относится к ситуациям, когда можно было видеть, что время жизни ссылок перекрывается, не имея возможности увидеть какие-либо отношения между ними. — person L.C.; 20.02.2019
Терминология — прекрасная вещь, я могу использовать ее, как хочу, как и все остальные!
Грубо говоря, типы похожи, когда они отличаются константностью или подписью. Одного размера в байтах явно недостаточно.
Воспроизведение текста — это любой метод, который позволяет обойти систему шрифтов.
Псевдонимы — это особый случай, когда объекты разных типов размещаются по одному и тому же адресу. Псевдонимы обычно разрешены, когда типы похожи, и запрещены в противном случае. Кроме того, можно получить доступ к объекту любого типа через
char
(или аналогичныйchar
) lvalue, но делать противоположное (т. Е. Доступ к объекту типаchar
через lvalue другого типа) не разрешается. Это гарантируется стандартами C и C ++, GCC просто реализует то, что предписано стандартами.Документация GCC, кажется, использует «каламбур типов» в узком смысле чтения члена объединения, отличного от последнего записанного. Этот вид каламбура разрешен стандартом C, даже если типы не похожи. OTOH стандарт C ++ не допускает этого. GCC может или не может расширять разрешение на C ++, в документации это неясно.
Без
-fstrict-aliasing
GCC, очевидно, ослабляет эти требования, но неясно, в какой именно степени. Обратите внимание, что-fstrict-aliasing
используется по умолчанию при выполнении оптимизированной сборки.Итог, просто программа по стандарту. Если GCC ослабит требования стандарта, это не имеет большого значения и не стоит усилий.
Авторы Стандарта сознательно позволяют специализированным реализациям вести себя так, чтобы они не подходили для большинства целей. Хотя 90% + оптимизаций, разрешенных
-fstrict-aliasing
, было бы разумно для реализации общего назначения, оставшиеся 10% (фальшивые оптимизации) делают этот режим непригодным для многих целей. — person L.C.; 20.02.2019В ANSI C (AKA C89) у вас есть (раздел 3.3.2.3 Элементы структуры и объединения):
В C99 у вас есть (раздел 6.5.2.3 Элементы структуры и объединения):
IOW, перфорирование типов на основе объединения разрешено в C, хотя фактическая семантика может отличаться в зависимости от поддерживаемого языкового стандарта (обратите внимание, что семантика C99 уже, чем C89 , определяемая реализацией).
В C99 у вас также есть (раздел 6.5 Выражения):
И есть раздел (6.2.7 Совместимый тип и составной тип) в C99, который описывает совместимые типы:
И затем (6.7.5.1 деклараторы указателя):
Немного упрощая это, это означает, что в C с помощью указателя вы можете получить доступ к целым числам со знаком как целым числам без знака (и наоборот), и вы можете получить доступ к отдельным символам в чем угодно. Все остальное будет равносильно нарушению псевдонима.
Вы можете найти аналогичный язык в различных версиях стандарта C ++. Однако, насколько я понимаю, в C ++ 03 и C ++ 11 каламбур на основе объединения явно не разрешен (в отличие от C).
УФ: этот ответ проясняет концепцию совместимых типов (я полагаю, это то, что они подразумевают под подобными типами). Я полностью согласен, что это явно не разрешено стандартом, но в некоторых случаях это работает с GCC. Это одна из ситуаций, когда «явно не разрешено» не означает «запрещено». — person L.C.; 19.02.2019
@ L.C. это не значит, что он не сломается внезапно на другом компиляторе, архитектуре, ОС или даже на новой версии компилятора. — person L.C.; 19.02.2019
Вы правы, все понятно … но создание кода с открытым исходным кодом, гибкого и переносимого и т. Д. Не всегда является основной целью. Это не элегантно, это не очень хорошая практика, но иногда просто нужен двоичный файл, который работает на текущей машине / ОС … поэтому, если компилятор создает правильный код, который делает то, что ожидается … почему бы и нет! — person L.C.; 19.02.2019
Согласно сноске 88 в проекте N1570 C11, «правило строгого псевдонима» (6.5p7) предназначено для указания обстоятельств, при которых компиляторы должны учитывать возможность того, что объекты могут иметь псевдоним, но не пытается определить, какой псевдоним есть. Где-то по ходу дела возникло распространенное мнение, что доступы, отличные от тех, которые определены правилом, представляют собой «псевдонимы», а разрешенные — нет, но на самом деле все наоборот.
Учитывая такую функцию, как:
В разделе 6.5p7 не сказано, что
p
иq
не будут использовать псевдонимы, если они идентифицируют одно и то же хранилище. Скорее, он указывает, что им разрешено использовать псевдоним.Обратите внимание, что не все операции, связанные с доступом к хранилищу одного типа в качестве другого, представляют собой псевдонимы. Операция над значением lvalue, которое явно является производным от другого объекта, не «псевдоним» этого другого объекта. Вместо этого это операция над этим объектом. Псевдоним возникает, если между моментом создания ссылки на какое-либо хранилище и временем его использования на это хранилище ссылаются каким-либо образом, не производным от первого, или код входит в контекст, в котором это происходит .
Хотя способность распознать, когда lvalue является производным от другого, является проблемой качества реализации, авторы стандарта должны были ожидать, что реализации будут распознавать некоторые конструкции, выходящие за рамки предписанных. Нет общего разрешения на доступ к любому хранилищу, связанному со структурой или объединением, с использованием lvalue типа члена, и ничто в Стандарте явно не говорит, что операция с участием
someStruct.member
должна распознаваться как операция наsomeStruct
. Вместо этого авторы Стандарта ожидали, что составители компиляторов, которые прилагают разумные усилия для поддержки конструкций, в которых нуждаются их клиенты, должны иметь больше возможностей, чем Комитет, для оценки потребностей этих клиентов и их удовлетворения. Поскольку любой компилятор, который делает хотя бы отдаленно разумные усилия для распознавания производных ссылок, заметит, чтоsomeStruct.member
является производным отsomeStruct
, авторы Стандарта не видели необходимости явно указывать это.К сожалению, обращение с конструкциями вроде:
эволюционировал от «Достаточно очевидно, что
actOnStruct
и разыменование указателя должны действовать наsomeUnion
(и, следовательно, на все его члены), что нет необходимости санкционировать такое поведение» до «Поскольку Стандарт не требует, чтобы реализации признавали, что указанные выше действия могут повлиять наsomeUnion
, любой код, основанный на таком поведении, нарушен и не нуждается в поддержке «. Ни одна из вышеперечисленных конструкций надежно не поддерживается gcc или clang, кроме режима-fno-strict-aliasing
, хотя большинство «оптимизаций», которые были бы заблокированы их поддержкой, генерировали бы код, который является «эффективным», но бесполезным.Если вы используете
-fno-strict-aliasing
на любом компиляторе, имеющем такую опцию, почти все будет работать. Если вы используете-fstrict-aliasing
на icc, он будет пытаться поддерживать конструкции, которые используют каламбур без псевдонимов, хотя я не знаю, есть ли какая-либо документация о том, какие именно конструкции он обрабатывает или не обрабатывает. Если вы используете-fstrict-aliasing
в gcc или clang, все, что работает, является чистой случайностью.Я думаю, что было бы хорошо добавить дополнительный ответ просто потому, что, когда я задавал вопрос, я не знал, как удовлетворить свои потребности без использования UNION: я упрямо использовал его, потому что он, казалось, отвечал именно моим потребностям.
Хороший способ выполнить каламбур типов и избежать возможных последствий неопределенного поведения (в зависимости от компилятора и других настроек окружения) — использовать std :: memcpy и копировать байты памяти из одного типа в другой. Это объясняется, например, здесь и здесь.
Я также читал, что часто, когда компилятор создает допустимый код для выделения типов с использованием объединений, он создает такой же двоичный код, как если бы использовался std :: memcpy.
Наконец, даже если эта информация не дает прямого ответа на мой первоначальный вопрос, она настолько тесно связана, что я счел полезным добавить ее сюда.