Обновление 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.
Относится ли результирующий указатель здесь к объекту указателя или значению указателя?
На мой взгляд, я думаю, что ответом является объект указателя, но больше ответов, похоже, указывают на значение указателя.
Мои мысли таковы: указатель сам по себе является объектом. Согласно 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");
}
}
Однако, если стандарт 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);
}
Какая интерпретация верна?
Хотя это хороший и хорошо написанный вопрос, мне интересно, почему вы его публикуете? Это просто любопытство (что нормально)? Это разъяснение того, что вы прочитали в спецификациях? Или есть какая-то другая основная проблема, которая приводит к этому вопросу? Если есть основная проблема, пожалуйста, отредактируйте свой вопрос, чтобы включить это.
Мы также знаем, что выравнивание более крупного объекта будет корректно выравниваться с меньшими объектами. т. е. указатель на что-то размером 8 можно преобразовать в что-то размера 4, но не наоборот. Таким образом, ваш пример выше в B неверен, поскольку buf имеет размер 4096, поэтому указатель int будет правильно выровнен. если buf был buf[2]
, то все ставки сняты.
@MartinYork Неправда. Выравнивание массива — это выравнивание базового типа объекта, поэтому buf
будет иметь требование выравнивания только 1, независимо от размера массива.
[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.
@dbush: Оппс, вы правы насчет массивов (при условии, что они локальны, как указано выше). Я был неправ в этом случае. Но в целом наблюдение верно.
На самом деле мне это кажется двусмысленным. Я не убежден с ответом tbh.
@MartinYork «Все обычные указатели имеют одинаковый размер». --> Указатели объектов и указатели функций часто могут иметь разные размеры. Различные указатели объектов могут различаться по размеру, но в наши дни это редкость.
@chux-ReinstateMonica: Вот почему я сказал «нормальный», а не «все» (поскольку я знал, что какой-нибудь адвокат попытается указать, что я не совсем прав). В контексте выше мы говорим об указателях на объекты.
Я думаю, что единственное различие, которое следует учитывать, - это различие между указателями и постоянными указателями. Указатели на любой тип всегда будут выравниваться, в то время как для постоянного указателя выравнивание идет только в более слабом (см. комментарии Мартина) направлении, а также я предполагаю, что они зависят от указанного типа (но я не уверен в этом последнем утверждении)
@ЕвгенийШ. - если [6.7.6 Alignment] Paragraph 5
относится к тому, о чем вы говорите, то что в нем кажется двусмысленным?
@ryyker Я имею в виду формулировку, процитированную ОП. Раньше я думал так же, как ответ dbush, но, снова глядя на цитируемый раздел, я думаю, что это не очень очевидно.
@Martin York: Вот почему у меня возникает эта проблема, если я исключаю указатели на функции и убеждаюсь, что указатели объектов имеют одинаковый размер и выравнивание. В интерпретации A я могу безопасно выполнять преобразование между любыми объектными указателями в качестве метода определения адреса.
@RichardBryant: Нет. Я указывал, что проверка бессмысленна (как это всегда верно). char* x = 0x01;
Действует. int* y = (int*)x;
Недействительно. Указатели x/y имеют одинаковое выравнивание, и вы можете присваивать им значения. Но присвоение недопустимого значения — это UB. Интерпретация Б верна.
Спасибо всем за исправление моего недоразумения, я понял!
@RichardBryant Смотрите мое редактирование. Есть и другие проблемы.
Интерпретация Б верна. Стандарт говорит об указателе на объект, а не о самом объекте. «Результирующий указатель» относится к результату приведения, а приведение не создает 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
в своем коде, чтобы указать, что это распределенные объекты. Но я хотел бы спросить, как только выделенный объект имеет эффективный тип, является ли незаконным преобразование его в другой тип (мы хотим повторно использовать буфер, когда нам не нужны старые данные)?
@RichardBryant На основании пункта 6 я считаю, что это приемлемо. Ключевая фраза заключается в том, что после сохранения значения «тип lvalue становится эффективным типом объекта для этого доступа и для последующих доступов, которые не изменяют сохраненное значение». Таким образом, сохранение нового значения может изменить тип.
@dbush: сохранение нового значения (битового шаблона, который никогда не хранился в хранилище) изменит эффективный тип, но в диалекте -fstrict-aliasing
, обрабатываемом clang и gcc, если область хранения использовалась для хранения T1 с определенным битовым шаблоном. , запись T2 с этим битовым шаблоном может привести к тому, что эффективный тип будет установлен на досуге компилятора либо в T1, либо в T2.
Стандарт не требует, чтобы реализации учитывали возможность того, что код когда-либо обнаружит значение в 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
.
Все обычные указатели имеют одинаковый размер.
sizeof(int*) == sizeof(char*)
. Поэтому все указатели будут иметь одинаковое выравнивание:_Alignof(int*) == _Alignof(char*)
поскольку выравнивание зависит от размера.