C - поведение преобразования между двумя указателями

Обновление 2020-12-11: спасибо @"какой-то чувак-программист" за предложение в комментарии. Моя основная проблема заключается в том, что наша команда реализует механизм хранения динамического типа. Мы выделяем несколько буферов массива символов [PAGE_SIZE] с выравниванием по 16 для хранения динамических типов данных (фиксированной структуры нет). Из соображений эффективности мы не можем выполнять кодирование байтов или выделять дополнительное пространство для использования memcpy.

Поскольку выравнивание было определено (т.е. 16), остальное — использовать приведение указателя для доступа к объектам указанного типа, например:

int main() {
    // simulate our 16-aligned malloc
    _Alignas(16) char buf[4096];

    // store some dynamic data:
    *((unsigned long *) buf) = 0xff07;
    *(((double *) buf) + 2) = 1.618;
}

Но наша команда оспаривает, является ли эта операция неопределенным поведением.


Я читал много подобных вопросов, таких как

Но это отличается от моей интерпретации стандарта C, я хочу знать, не мое ли это непонимание.

Основная путаница связана с разделом 6.3.2.3 #7 C11:

Указатель на тип объекта может быть преобразован в указатель на другой тип объекта. Если результирующий указатель неправильно выровнен 68) для типа, на который ссылаются, поведение не определено.

68) В общем, понятие «правильно выровненный» является транзитивным: если указатель на тип A правильно выровнен для указателя на тип B, который, в свою очередь, правильно выровнен для указателя на тип C, то указатель на тип A правильно выровнен для указателя на тип C.

Относится ли результирующий указатель здесь к объекту указателя или значению указателя?

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


Интерпретация A: объект-указатель

Мои мысли таковы: указатель сам по себе является объектом. Согласно 6.2.5 #28, разные указатели могут иметь разные требования к представлению и выравниванию. Следовательно, согласно 6.3.2.3 #7, пока два указателя имеют одинаковое выравнивание, их можно безопасно преобразовать без неопределенного поведения, но нет гарантии, что их можно будет разыменовать. Выразите эту идею в программе:

#include <stdio.h>

int main() {
    char buf[4096];

    char *pc = buf;
    if (_Alignof(char *) == _Alignof(int *)) {
        // cast safely, because they have the same alignment requirement?
        int *pi = (int *) pc; 
        printf("pi: %p\n", pi);
    } else {
        printf("char * and int * don't have the same alignment.\n");
    }
}

Интерпретация B: значение указателя

Однако, если стандарт C11 говорит о значении указателя для ссылочного типа, а не об объекте указателя. Проверка выравнивания приведенного выше кода бессмысленна. Выразите эту идею в программе:

#include <stdio.h>

int main() {
    char buf[4096];

    char *pc = buf;
    
    /*
     * undefined behavior, because:
     * align of char is 1
     * align of int is 4
     * 
     * and we don't know whether the `value` of pc is 4-aligned.
     */
    int *pi = (int *) pc;
    printf("pi: %p\n", pi);
}

Какая интерпретация верна?

Все обычные указатели имеют одинаковый размер. sizeof(int*) == sizeof(char*). Поэтому все указатели будут иметь одинаковое выравнивание: _Alignof(int*) == _Alignof(char*) поскольку выравнивание зависит от размера.

Martin York 10.12.2020 19:36

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

Some programmer dude 10.12.2020 19:37

Мы также знаем, что выравнивание более крупного объекта будет корректно выравниваться с меньшими объектами. т. е. указатель на что-то размером 8 можно преобразовать в что-то размера 4, но не наоборот. Таким образом, ваш пример выше в B неверен, поскольку buf имеет размер 4096, поэтому указатель int будет правильно выровнен. если buf был buf[2], то все ставки сняты.

Martin York 10.12.2020 19:39

@MartinYork Неправда. Выравнивание массива — это выравнивание базового типа объекта, поэтому buf будет иметь требование выравнивания только 1, независимо от размера массива.

dbush 10.12.2020 19:41

[6.7.6 Выравнивание] Параграф 5: Alignments have an order from weaker to stronger or stricter alignments. Stricter alignments have larger alignment values. An address that satisfies an alignment requirement also satisfies any weaker valid alignment requirement.

Martin York 10.12.2020 19:42

@dbush: Оппс, вы правы насчет массивов (при условии, что они локальны, как указано выше). Я был неправ в этом случае. Но в целом наблюдение верно.

Martin York 10.12.2020 19:44

На самом деле мне это кажется двусмысленным. Я не убежден с ответом tbh.

Eugene Sh. 10.12.2020 19:45

@MartinYork «Все обычные указатели имеют одинаковый размер». --> Указатели объектов и указатели функций часто могут иметь разные размеры. Различные указатели объектов могут различаться по размеру, но в наши дни это редкость.

chux - Reinstate Monica 10.12.2020 19:50

@chux-ReinstateMonica: Вот почему я сказал «нормальный», а не «все» (поскольку я знал, что какой-нибудь адвокат попытается указать, что я не совсем прав). В контексте выше мы говорим об указателях на объекты.

Martin York 10.12.2020 19:52

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

anotherOne 10.12.2020 19:54

@ЕвгенийШ. - если [6.7.6 Alignment] Paragraph 5 относится к тому, о чем вы говорите, то что в нем кажется двусмысленным?

ryyker 10.12.2020 20:05

@ryyker Я имею в виду формулировку, процитированную ОП. Раньше я думал так же, как ответ dbush, но, снова глядя на цитируемый раздел, я думаю, что это не очень очевидно.

Eugene Sh. 10.12.2020 20:14

@Martin York: Вот почему у меня возникает эта проблема, если я исключаю указатели на функции и убеждаюсь, что указатели объектов имеют одинаковый размер и выравнивание. В интерпретации A я могу безопасно выполнять преобразование между любыми объектными указателями в качестве метода определения адреса.

Richard Bryant 10.12.2020 20:30

@RichardBryant: Нет. Я указывал, что проверка бессмысленна (как это всегда верно). char* x = 0x01; Действует. int* y = (int*)x; Недействительно. Указатели x/y имеют одинаковое выравнивание, и вы можете присваивать им значения. Но присвоение недопустимого значения — это UB. Интерпретация Б верна.

Martin York 10.12.2020 20:59

Спасибо всем за исправление моего недоразумения, я понял!

Richard Bryant 10.12.2020 21:07

@RichardBryant Смотрите мое редактирование. Есть и другие проблемы.

dbush 10.12.2020 22:11
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
9
16
452
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Интерпретация Б верна. Стандарт говорит об указателе на объект, а не о самом объекте. «Результирующий указатель» относится к результату приведения, а приведение не создает lvalue, поэтому оно относится к значению указателя после приведения.

Взяв код из вашего примера, предположим, что int должен быть выровнен по 4-байтовой границе, т. е. его адрес должен быть кратным 4. Если адрес buf равен 0x1001, то преобразование этого адреса в int * недопустимо, поскольку значение указателя равно не выровнено должным образом. Если адрес buf равен 0x1000, то его преобразование в int * допустимо.

Обновлять:

Код, который вы добавили, решает проблему выравнивания, так что в этом отношении все в порядке. Однако у него другая проблема: он нарушает строгое сглаживание.

Определенный вами массив содержит объекты типа char. Преобразуя адрес в другой тип и впоследствии разыменовывая преобразованный тип, вы получаете доступ к объектам одного типа как к объектам другого типа. Это не разрешено стандартом C.

Хотя термин «строгий псевдоним» не используется в стандарте, концепция описана в параграфах 6 и 7 раздела 6.5:

6 Эффективным типом объекта для доступа к его хранимому значению является объявленный тип объекта, если таковой имеется. значение сохраняется в объекте, не имеющем объявленного типа, через lvalue, имеющее тип, отличный от символьного типа, то тип lvalue становится эффективным типом объекта для этого доступа и для последующих обращений, которые не изменяют сохраненное значение. Если значение копируется в объект, не имеющий объявленного типа, с помощью memcpy или memmove, или копируется как массив символов, то эффективный тип измененного объекта для этого доступа и для последующие обращения, которые не изменяют значение, являются эффективным типом объекта, из которого копируется значение, если оно имеется. Для всех другие обращения к объекту, не имеющему объявленного типа, эффективный тип объекта - это просто тип lvalue, используемый для доступ.

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

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

...

87) Размещенные объекты не имеют объявленного типа.

88) Цель этого списка состоит в том, чтобы указать те обстоятельства, при которых объект может иметь или не иметь псевдоним.

В вашем примере вы пишете unsigned long и double поверх char объектов. Ни один из этих типов не удовлетворяет условиям параграфа 7.

Кроме того, арифметика указателя здесь недействительна:

 *(((double *) buf) + 2) = 1.618;

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

Так почему же это проблема для массива char, а не для буфера, возвращаемого malloc? Потому что память, возвращаемая из malloc, не имеет эффективного типа, пока вы не сохраните что-то в ней, что и описано в параграфе 6 и сноске 87.

Итак, со строгой точки зрения стандарта то, что вы делаете, является неопределенным поведением. Но в зависимости от вашего компилятора вы можете отключить строгие псевдонимы, чтобы это сработало. Если вы используете gcc, вам нужно передать флаг -fno-strict-aliasing

Спасибо за обновление! Я понимаю основные строгие псевдонимы, поэтому я комментирую simulate our 16-aligned malloc в своем коде, чтобы указать, что это распределенные объекты. Но я хотел бы спросить, как только выделенный объект имеет эффективный тип, является ли незаконным преобразование его в другой тип (мы хотим повторно использовать буфер, когда нам не нужны старые данные)?

Richard Bryant 11.12.2020 00:50

@RichardBryant На основании пункта 6 я считаю, что это приемлемо. Ключевая фраза заключается в том, что после сохранения значения «тип lvalue становится эффективным типом объекта для этого доступа и для последующих доступов, которые не изменяют сохраненное значение». Таким образом, сохранение нового значения может изменить тип.

dbush 11.12.2020 01:02

@dbush: сохранение нового значения (битового шаблона, который никогда не хранился в хранилище) изменит эффективный тип, но в диалекте -fstrict-aliasing, обрабатываемом clang и gcc, если область хранения использовалась для хранения T1 с определенным битовым шаблоном. , запись T2 с этим битовым шаблоном может привести к тому, что эффективный тип будет установлен на досуге компилятора либо в T1, либо в T2.

supercat 12.12.2020 19:06

Стандарт не требует, чтобы реализации учитывали возможность того, что код когда-либо обнаружит значение в T*, которое не выровнено для типа T. В clang, например, при нацеливании на платформы, чьи «крупные» инструкции загрузки/сохранения не поддерживают невыровненный доступ. , преобразование указателя в тип, выравнивание которого он не удовлетворяет, а затем использование memcpy для него может привести к тому, что компилятор сгенерирует код, который завершится ошибкой, если указатель не выровнен, даже если сам memcpy в противном случае не наложил бы никаких требований к выравниванию.

Например, при нацеливании на ARM Cortex-M0 или Cortex-M3 с учетом:

void test1(long long *dest, long long *src)
{
    memcpy(dest, src, sizeof (long long));
}
void test2(char *dest, char *src)
{
    memcpy(dest, src, sizeof (long long));
}
void test3(long long *dest, long long *src)
{
    *dest = *src;
}

clang сгенерирует код как для test1, так и для test3, который потерпит неудачу, если src или dest не будут выровнены, но для test2 он сгенерирует код, который больше и медленнее, но который будет поддерживать произвольное выравнивание операндов источника и назначения.

Конечно, даже при clang преобразование невыровненного указателя в long long* само по себе обычно не вызывает ничего странного, но тот факт, что такое преобразование приведет к созданию UB, освобождает компилятор от какой-либо ответственности за обработку. случай невыровненного указателя в test1.

Другие вопросы по теме