Объединения, псевдонимы и подбор текста на практике: что работает, а что нет?

У меня проблема с пониманием того, что можно, а что нельзя делать с помощью объединений с 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): алиасинг — как следует из этого слова — это когда у вас есть одно значение и два члена для доступа к нему (то есть одни и те же байты);

Сомнение: ли два типа похожи, если они имеют одинаковый размер в байтах? Если нет, то какие бывают похожие типы?

См. также:  Компиляция 32-битных программ и вызов 64-битных программ в 64-битных системах

Следствие 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

Понравилась статья? Поделиться с друзьями:
IT Шеф
Комментарии: 5
  1. L.C.

    Псевдонимы можно понимать буквально: это когда два разных выражения относятся к одному и тому же объекту. Каламбур — это «каламбур» типа, то есть использование объекта одного типа в качестве другого типа.

    Формально, каламбур — это неопределенное поведение, за редким исключением. Обычно это случается, когда вы небрежно возитесь с битами

    int mantissa(float f)
    {
        return (int&)f & 0x7FFFFF;    // Accessing a float as if it's an int
    }
    

    Исключения (упрощенные)

    • Доступ к целым числам как к их беззнаковым / подписанным аналогам
    • Доступ к чему угодно как char, unsigned char или std::byte

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

    void transform(float* dst, const int* src, int n)
    {
        for(int i = 0; i < n; i++)
            dst[i] = src[i];    // Can be unrolled and use vector instructions
                                // If dst and src alias the results would be wrong
    }
    

    Что говорит gcc, так это то, что он немного смягчает правила и позволяет набирать типы через объединения, даже если стандарт не требует, чтобы

    union {
        int64_t num;
        struct {
            int32_t hi, lo;
        } parts;
    } u = {42};
    u.parts.hi = 420;
    

    Это тип-каламбур, который гарантирует 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

  2. L.C.

    Терминология — прекрасная вещь, я могу использовать ее, как хочу, как и все остальные!

    похожи ли два типа, если они имеют одинаковый размер в байтах? Если нет, то какие бывают похожие типы?

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

    псевдоним является конкретным случаем каламбура, когда типы похожи?

    Воспроизведение текста — это любой метод, который позволяет обойти систему шрифтов.

    Псевдонимы — это особый случай, когда объекты разных типов размещаются по одному и тому же адресу. Псевдонимы обычно разрешены, когда типы похожи, и запрещены в противном случае. Кроме того, можно получить доступ к объекту любого типа через 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

  3. L.C.

    В ANSI C (AKA C89) у вас есть (раздел 3.3.2.3 Элементы структуры и объединения):

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

    В C99 у вас есть (раздел 6.5.2.3 Элементы структуры и объединения):

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

    IOW, перфорирование типов на основе объединения разрешено в C, хотя фактическая семантика может отличаться в зависимости от поддерживаемого языкового стандарта (обратите внимание, что семантика C99 уже, чем C89 , определяемая реализацией).

    В C99 у вас также есть (раздел 6.5 Выражения):

    Доступ к сохраненному значению объекта должен осуществляться только выражением lvalue, которое имеет один из следующих типов:

    — тип, совместимый с эффективным типом объекта,

    — квалифицированная версия типа, совместимого с действующим типом объекта,

    — тип, который представляет собой знаковый или беззнаковый тип, соответствующий действующему типу объекта,

    — тип, который является типом со знаком или без знака, соответствующим квалифицированной версии действующего типа объекта,

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

    — символьный тип.

    И есть раздел (6.2.7 Совместимый тип и составной тип) в C99, который описывает совместимые типы:

    Два типа имеют совместимый тип, если их типы совпадают. Дополнительные правила для определения совместимости двух типов описаны в 6.7.2 для спецификаторов типов, в 6.7.3 для квалификаторов типов и в 6.7.5 для деклараторов. …

    И затем (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

  4. L.C.

    Согласно сноске 88 в проекте N1570 C11, «правило строгого псевдонима» (6.5p7) предназначено для указания обстоятельств, при которых компиляторы должны учитывать возможность того, что объекты могут иметь псевдоним, но не пытается определить, какой псевдоним есть. Где-то по ходу дела возникло распространенное мнение, что доступы, отличные от тех, которые определены правилом, представляют собой «псевдонимы», а разрешенные — нет, но на самом деле все наоборот.

    Учитывая такую ​​функцию, как:

    int foo(int *p, int *q)
    { *p = 1; *q = 2; return *p; }
    

    В разделе 6.5p7 не сказано, что p и q не будут использовать псевдонимы, если они идентифицируют одно и то же хранилище. Скорее, он указывает, что им разрешено использовать псевдоним.

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

    Хотя способность распознать, когда lvalue является производным от другого, является проблемой качества реализации, авторы стандарта должны были ожидать, что реализации будут распознавать некоторые конструкции, выходящие за рамки предписанных. Нет общего разрешения на доступ к любому хранилищу, связанному со структурой или объединением, с использованием lvalue типа члена, и ничто в Стандарте явно не говорит, что операция с участием someStruct.member должна распознаваться как операция на someStruct. Вместо этого авторы Стандарта ожидали, что составители компиляторов, которые прилагают разумные усилия для поддержки конструкций, в которых нуждаются их клиенты, должны иметь больше возможностей, чем Комитет, для оценки потребностей этих клиентов и их удовлетворения. Поскольку любой компилятор, который делает хотя бы отдаленно разумные усилия для распознавания производных ссылок, заметит, что someStruct.member является производным от someStruct, авторы Стандарта не видели необходимости явно указывать это.

    К сожалению, обращение с конструкциями вроде:

    actOnStruct(&someUnion.someStruct);
    int q=*(someUnion.intArray+i)
    

    эволюционировал от «Достаточно очевидно, что actOnStruct и разыменование указателя должны действовать на someUnion (и, следовательно, на все его члены), что нет необходимости санкционировать такое поведение» до «Поскольку Стандарт не требует, чтобы реализации признавали, что указанные выше действия могут повлиять на someUnion, любой код, основанный на таком поведении, нарушен и не нуждается в поддержке «. Ни одна из вышеперечисленных конструкций надежно не поддерживается gcc или clang, кроме режима -fno-strict-aliasing, хотя большинство «оптимизаций», которые были бы заблокированы их поддержкой, генерировали бы код, который является «эффективным», но бесполезным.

    Если вы используете -fno-strict-aliasing на любом компиляторе, имеющем такую ​​опцию, почти все будет работать. Если вы используете -fstrict-aliasing на icc, он будет пытаться поддерживать конструкции, которые используют каламбур без псевдонимов, хотя я не знаю, есть ли какая-либо документация о том, какие именно конструкции он обрабатывает или не обрабатывает. Если вы используете -fstrict-aliasing в gcc или clang, все, что работает, является чистой случайностью.

  5. L.C.

    Я думаю, что было бы хорошо добавить дополнительный ответ просто потому, что, когда я задавал вопрос, я не знал, как удовлетворить свои потребности без использования UNION: я упрямо использовал его, потому что он, казалось, отвечал именно моим потребностям.

    Хороший способ выполнить каламбур типов и избежать возможных последствий неопределенного поведения (в зависимости от компилятора и других настроек окружения) — использовать std :: memcpy и копировать байты памяти из одного типа в другой. Это объясняется, например, здесь и здесь.

    Я также читал, что часто, когда компилятор создает допустимый код для выделения типов с использованием объединений, он создает такой же двоичный код, как если бы использовался std :: memcpy.

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

Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: