При ответе на другой вопрос я думал о следующем примере:
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, не нарушая правила искажения?
Стандарт 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 (люди в этом списке не согласны что это).