Подтвердить что ты не робот

Можно ли использовать memcpy для кастомизации типа?

Это цитата из стандарта C11:

6.5 Выражения
...

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

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

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

Означает ли это, что memcpy нельзя использовать для обозначения типа таким образом:

double d = 1234.5678;
uint64_t bits;
memcpy(&bits, &d, sizeof bits);
printf("the representation of %g is %08"PRIX64"\n", d, bits);

Почему бы не дать такой же вывод, как:

union { double d; uint64_t i; } u;
u.d = 1234.5678;
printf("the representation of %g is %08"PRIX64"\n", d, u.i);

Что если я использую свою версию memcpy с использованием типов символов:

void *my_memcpy(void *dst, const void *src, size_t n) {
    unsigned char *d = dst;
    const unsigned char *s = src;
    for (size_t i = 0; i < n; i++) { d[i] = s[i]; }
    return dst;
}

РЕДАКТИРОВАТЬ: EOF отметил, что часть о memcpy() в параграфе 6 не применяется в этой ситуации, так uint64_t bits имеют объявленный тип. Я согласен, но, к сожалению, это не помогает ответить на вопрос, можно ли использовать memcpy для наказания типов, просто делает пункт 6 неуместным для оценки достоверности приведенных выше примеров.

Вот еще одна попытка типа Punning с memcpy которая, я думаю, будет рассмотрена в пункте 6:

double d = 1234.5678;
void *p = malloc(sizeof(double));
if (p != NULL) {
    uint64_t *pbits = memcpy(p, &d, sizeof(double));
    uint64_t bits = *pbits;
    printf("the representation of %g is %08"PRIX64"\n", d, bits);
}

Предполагая, что sizeof(double) == sizeof(uint64_t), имеет ли приведенный выше код определенное поведение в параграфах 6 и 7?


РЕДАКТИРОВАТЬ: Некоторые ответы указывают на потенциал неопределенного поведения, возникающего при чтении представления ловушки. Это не имеет значения, поскольку Стандарт C явно исключает эту возможность:

7.20.1.1 Целочисленные типы с точной шириной

1 Имя typedef int N _t обозначает целочисленный тип со знаком с шириной N, без дополнительных битов и представление с дополнением до двух. Таким образом, int8_t обозначает такой целочисленный тип со int8_t шириной ровно 8 бит.

2 Имя определения типа uint N _t обозначает целочисленный тип без знака с шириной N и без битов заполнения. Таким образом, uint24_t обозначает такой целочисленный тип без знака с шириной ровно 24 бита.

Эти типы не являются обязательными. Однако, если реализация предоставляет целочисленные типы с шириной 8, 16, 32 или 64 бита, без битов заполнения и (для подписанных типов), которые имеют представление дополнения до двух, она должна определить соответствующие имена typedef.

Тип uint64_t имеет ровно 64 значения битов и не имеет битов заполнения, поэтому не может быть никаких представлений прерываний.

4b9b3361

Ответ 1

Есть два случая, чтобы рассмотреть: memcpy() в объект, который имеет объявленный тип, и memcpy() в объект, который не имеет.

Во втором случае

double d = 1234.5678;
void *p = malloc(sizeof(double));
assert(p);
uint64_t *pbits = memcpy(p, &d, sizeof(double));
uint64_t bits = *pbits;
printf("the representation of %g is %08"PRIX64"\n", d, bits);

Поведение действительно не определено, так как эффективный тип объекта, на который указывает p, станет double, а доступ к объекту эффективного типа double хотя lvalue типа uint64_t не определено.

С другой стороны,

double d = 1234.5678;
uint64_t bits;
memcpy(&bits, &d, sizeof bits);
printf("the representation of %g is %08"PRIX64"\n", d, bits);

не является неопределенным Проект стандарта C11 N1570:

7.24.1 Соглашения о строковых функциях
3 Для всех функций в этом подпункте каждый символ должен интерпретироваться так, как если бы он имел тип unsigned char (и, следовательно, каждое возможное представление объекта является допустимым и имеет другое значение).

А также

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

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

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

Таким образом, сама memcpy() четко определена.

Поскольку uint64_t bits имеют объявленный тип, он сохраняет свой тип, даже если его объектное представление было скопировано из double.

Как указывает chqrlie, uint64_t не может иметь представления ловушек, поэтому доступ к bits после memcpy() не является неопределенным при условии sizeof(uint64_t) == sizeof(double). Однако значение bits будет зависеть от реализации (например, из-за порядкового номера).

Вывод: memcpy() можно использовать для определения типа, при условии, что у назначения memcpy() действительно есть объявленный тип, то есть он не выделяется с помощью [m/c/re]alloc() или его эквивалента.

Ответ 2

Вы предлагаете 3 способа, которые имеют разные проблемы со стандартом C.

  • стандартная библиотека memcpy

    double d = 1234.5678;
    uint64_t bits;
    memcpy(&bits, &d, sizeof bits);
    printf("the representation of %g is %08"PRIX64"\n", d, bits);
    

    Часть memcpy является законной (предоставляется в вашей реализации sizeof(double) == sizeof(uint64_t), которая не гарантирована для каждого стандарта): вы получаете доступ к двум объектам с помощью указателей char.

    Но строка printf отсутствует. Представление в bits теперь является двойным. это может быть ловушечное представление для uint64_t, как определено в 6.2.6.1. Общий §5

    Определенные представления объектов не обязательно должны представлять значение типа объекта. Если сохраненный значение объекта имеет такое представление и считывается выражением lvalue, которое делает не имеют характера, поведение undefined. Если такое представление создается побочным эффектом, который изменяет всю или любую часть объекта с помощью выражения lvalue, которое не имеет характера, поведение undefined. Такое представление называется представление ловушки.

    И 6.2.6.2. Типы Integer явно указывают

    Для беззнаковых целочисленных типов, отличных от unsigned char, биты объекта представление делится на две группы: биты значений и биты дополнений... Значения любых битов дополнений не определены. 53

    С примечанием 53, говорящим:

    Некоторые комбинации битов дополнений могут генерировать ловушки,

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

  • объединение

    union { double d; uint64_t i; } u;
    u.d = 1234.5678;
    printf("the representation of %g is %08"PRIX64"\n", d, u.i);
    

    Члены профсоюза не имеют общей подпоследовательности, и вы получаете доступ к члену, который не является последним записанным значением. Хорошо, что обычная реализация даст ожидаемые результаты, но по стандарту четко не определено, что должно произойти. Сноска в 6.5.2.3. Структура и члены профсоюза §3 говорит, что если приводит к тем же проблемам, что и предыдущий случай:

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

  • custom memcpy

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

Единственный способ, который будет явно определен для каждого стандарта, состоял в том, чтобы сохранить представление double в массиве char правильного размера, а затем отобразить значения байтов массива char:

double d = 1234.5678;
unsigned char bits[sizeof(d)];
memcpy(&bits, &d, sizeof(bits));
printf("the representation of %g is ", d);
for(int i=0; i<sizeof(bits); i++) {
    printf("%02x", (unsigned int) bits[i]);
}
printf("\n");

И результат будет полезен, только если реализация использует ровно 8 бит для char. Но это было бы видно, потому что оно отображало бы более 8 шестнадцатеричных цифр, если один из байтов имел значение больше 255.


Все вышесказанное верно только потому, что bits имеет объявленный тип. См. @EOF answer, чтобы понять, почему он будет отличаться для выделенного объекта

Ответ 3

Я читаю параграф 6, говоря, что использование функции memcpy() для копирования ряда байтов из одной ячейки памяти в другую ячейку памяти может использоваться для пин-кода типа, так как можно использовать union с двумя разными типами для кастомизации типа.

Первое упоминание об использовании memcpy() означает, что если он копирует указанное количество байтов и что эти байты будут иметь тот же тип, что и переменная в исходном месте назначения, когда эта переменная (lvalue) использовалась для хранения байтов там.

Другими словами, если у вас есть переменная double d;, и тогда вы присваиваете значение этой переменной (lvalue), тип данных, хранящихся в этой переменной, имеет тип double. Если вы затем используете функцию memcpy() для копирования этих байтов в другую ячейку памяти, скажем, переменная uint64_t bits;, тип этих скопированных байтов все еще double.

Если вы затем получаете доступ к скопированным байтам через целевую переменную (lvalue), uint64_t bits; в примере, то тип этих данных рассматривается как тип значения l, используемого для извлечения байтов данных из этой целевой переменной, Таким образом, байты интерпретируются (не преобразуются, а интерпретируются) как тип переменной назначения, а не тип исходной переменной.

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

Это также работает union. A union не выполняет никакого преобразования. Вы храните байты в члене union, который имеет один тип, а затем вы извлекаете одни и те же байты через другой член union. Байт один и тот же, однако интерпретация байтов зависит от типа члена union, который используется для доступа к области памяти.

Я видел функцию memcpy(), используемую в более раннем исходном коде C, чтобы разделить struct на части, используя struct смещение члена вместе с функцией memcpy() для копирования частей переменной struct в другие переменные struct.

Поскольку тип расположения источника, используемый в memcpy(), является типом хранимых там байтов, те же проблемы, с которыми вы можете столкнуться с использованием union для казни, также применимы к использованию memcpy() таким образом, как Endianness типа данных.

Следует помнить, что если использовать union или использовать подход memcpy(), тип скопированных байтов - это тип исходной переменной, и когда вы затем обращаетесь к данным как к другому типу, будь то через другой член переменной union или через целевую переменную memcpy() байты интерпретируются как тип значения l назначения назначения. Однако фактические байты не изменяются.

Ответ 4

ИЗМЕНИТЬ - СМОТРЕТЬ НИЖЕ

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

Что касается того, что это означает, что лучше использовать memcpy или ручной цикл byte-copy, цель которого достаточно хорошо замаскирована, чтобы быть неузнаваемой как "копирование массива типа символа", я понятия не имею. Я бы сказал, что разумным было бы избегать любого настолько тупого, чтобы предположить, что хороший компилятор должен генерировать фиктивный код, отсутствующий в таком обфускации, но так как поведение, которое считалось бы тупым в прошлые годы, в настоящее время модно, я понятия не имею, memcpy станет следующей жертвой гонки, чтобы разбить код, который составители на протяжении десятилетий рассматривали как "четко определенные".

UPDATE

GCC начиная с 6.2 иногда пропускает операции memmove в тех случаях, когда видит, что адресат и источник идентифицируют один и тот же адрес, даже если они являются указателями разных типов. Если хранилище, которое было записано как тип источника, позже будет считаться типом адресата, gcc будет считать, что последние чтения не могут идентифицировать то же хранилище, что и более ранняя запись. Такое поведение на gcc-части оправдано только из-за языка в Стандарте, который позволяет компилятору копировать Эффективный Тип через memmove. Неясно, было ли это преднамеренной интерпретацией правил, касающихся memcpy, однако, учитывая, что gcc также сделает аналогичную оптимизацию в некоторых случаях, когда стандарт явно не допускается, например, когда член объединения одного типа (например, 64-разрядный long) копируется во временный и оттуда к члену другого типа с тем же представлением (например, 64-бит long long). Если gcc видит, что адресат будет бит-бит, идентичный временному, он опустит запись и, следовательно, не заметит, что был изменен эффективный тип хранилища.

Ответ 5

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