C искажающие правила и memcpy

При ответе на другой вопрос я думал о следующем примере:

void *p;
unsigned x = 17;

assert(sizeof(void*) >= sizeof(unsigned));
*(unsigned*)&p = 17;        // (1)
memcpy(&p, &x, sizeof(x));  // (2)

Строка 1 нарушает правила искажения. Строка 2, однако, в порядке wrt. искажающие правила. Вопрос: почему? Компилятор имеет специальное встроенное знание о функциях, таких как memcpy или является там некоторыми другими правилами, которые делают memcpy хорошо? Существует ли способ реализовать подобные memcpy функции в стандарте C, не нарушая правила искажения?

12
задан zvrba 18 July 2010 в 11:41
поделиться

1 ответ

Стандарт C довольно четко описывает это. Эффективным типом объекта, названного p , является void * , поскольку он имеет объявленный тип, см. 6.5 / 6 . Правила наложения в C99 применяются к чтениям и записи, а запись в void * через беззнаковое lvalue в (1) равно неопределенное поведение согласно 6.5 / 7 .

Напротив, memcpy из (2) подходит, потому что unsigned char * может быть псевдонимом любого объекта ( 6.5 / 7 ]). Стандарт определяет memcpy в 7.21.2 / 1 как

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

Функция memcpy копирует n символов из объекта, на который указывает s2, в объект, на который указывает s1. Если копирование происходит между перекрывающимися объектами, поведение не определено.

Однако, если впоследствии будет использоваться p , это может привести к неопределенному поведению в зависимости от битового шаблона. Если такого использования не происходит, этот код подходит для C.


Согласно Стандарту C ++ , который, на мой взгляд, далеко не ясен по проблеме, я думаю, что справедливо следующее. Пожалуйста, не принимайте эту интерпретацию как единственно возможную - расплывчатая / неполная спецификация оставляет много места для предположений.

Строка (1) проблематична, потому что выравнивание & p может быть недопустимым для типа без знака . Он изменяет тип объекта, хранящегося в p , на unsigned int . Пока вы не обращаетесь к этому объекту позже через p , правила псевдонима не нарушаются, но требования к выравниванию все еще могут быть нарушены.

Строка (2) , однако, не имеет проблем с выравниванием и, таким образом, действительна, пока вы не обращаетесь к p впоследствии как void * , что может вызвать неопределенное поведение в зависимости от того, как тип void * интерпретирует сохраненный битовый шаблон. Не думаю, что при этом изменится тип объекта.

Существует длинный GCC Bugreport , в котором также обсуждаются последствия записи через указатель, которая возникла в результате такого преобразования, и в чем отличие от place-new (люди в этом списке не согласны что это).

14
ответ дан 2 December 2019 в 21:02
поделиться