Действительно ли строгое правило псевдонимов - это улица с двусторонним движением?
В этих комментариях пользователь @Deduplicator настаивает на том, что правило строгого псевдонима разрешает доступ через несовместимый тип, если указатель с псевдонимом или псевдонимом является указателем на символьный тип (квалифицированный или неквалифицированный, подписанный или неподписанный). char *
). Таким образом, его утверждение в основном заключается в том, что оба
long long foo;
char *p = (char *)&foo;
*p; // just in order to dereference 'p'
а также
char foo[sizeof(long long)];
long long *p = (long long *)&foo[0];
*p; // just in order to dereference 'p'
соответствуют и имеют определенное поведение.
Однако в моем прочтении допустима только первая форма, то есть когда указатель псевдонима является указателем на символ; однако, это невозможно сделать в другом направлении, то есть когда указатель псевдонимов указывает на несовместимый тип (кроме символьного типа), причем указатель- псевдоним является char *
,
Итак, второй фрагмент выше будет иметь неопределенное поведение.
В чем дело? Это правильно? Для справки, я уже прочитал этот вопрос и ответ, и там принятый ответ прямо заявляет, что
Правила допускают исключение для
char *
, Всегда предполагается, чтоchar *
псевдонимы других типов. Однако иначе это не сработает, нет предположения, что ваша структура псевдонимов представляет собой буфер символов.
(акцент мой)
2 ответа
Вы правильно сказали, что это недействительно. Как вы сами цитировали (поэтому я не буду здесь повторять), гарантированное действительное приведение только от любого другого типа к типу char*.
Другая форма действительно противоречит стандарту и вызывает неопределенное поведение. Однако в качестве небольшого бонуса давайте немного поговорим за этим стандартом.
Символы в каждой существенной архитектуре являются единственным типом, который разрешает полностью невыровненный доступ, это связано с тем, что инструкции чтения байта должны работать с любым байтом, иначе они будут практически бесполезны. Это означает, что косвенное чтение на символ всегда будет действительным на каждом известном мне процессоре.
Однако, наоборот, это не применимо, вы не можете прочитать uint64_t, если указатель не выровнен по 8 байтам на большинстве арок.
Тем не менее, существует очень распространенное расширение компилятора, позволяющее вам приводить правильно выровненные указатели из char в другие типы и получать к ним доступ, однако это нестандартно. Также обратите внимание, что если вы приведете указатель к любому типу к указателю на тип char, а затем приведете его обратно, результирующий указатель гарантированно будет равен исходному объекту. Поэтому это нормально:
struct x *mystruct = MakeAMyStruct();
char * foo = (char *)mystruct;
struct x *mystruct2 = (struct mystruct *)foo;
И mystruct2 будет равен mystruct. Это также гарантирует, что структура правильно выровнена для своих нужд.
Поэтому, если вам нужен указатель на тип char и указатель на другой тип, всегда объявляйте указатель на другой тип, а затем приводите его к типу char. Или даже лучше использовать союз, вот для чего они в основном...
Обратите внимание, что есть заметное исключение из правила. Некоторые старые реализации malloc использовали для возврата char*. Этот указатель всегда гарантированно может быть успешно приведен к любому типу без нарушения правил наложения имен.
Дедупликатор правильный. Неопределенное поведение, которое позволяет компиляторам реализовывать оптимизацию "строгого алиасинга", не применяется, когда символьные значения используются для создания представления объекта.
Определенные представления объекта не должны представлять значение типа объекта. Если сохраненное значение объекта имеет такое представление и читается выражением lvalue, которое не имеет символьного типа, поведение не определено. Если такое представление создается побочным эффектом, который изменяет весь или любую часть объекта выражением lvalue, которое не имеет символьного типа, поведение не определено. Такое представление называется представлением ловушек.
Однако ваш второй пример имеет неопределенное поведение, потому что foo
неинициализирован. Если вы инициализируете foo
тогда он имеет только поведение, определяемое реализацией Это зависит от реализации определенных требований выравнивания long long
и будь long long
имеет любую реализацию, определенную битами pad.
Подумайте, измените ли вы второй пример на этот:
long long bar() {
char *foo = malloc(sizeof(long long));
char c;
for(c = 0; c < sizeof(long long); c++)
foo[c] = c;
long long *p = (long long *) p;
return *p;
}
Теперь выравнивание больше не является проблемой, и этот пример зависит только от определенного реализацией представления long long
, Какое значение возвращается, зависит от представления long long
но если это представление определено как не имеющее битов-паддеров, эта функция всегда должна возвращать одно и то же значение и всегда должно быть допустимым значением. Без битов падов эта функция не может генерировать представление ловушек, и поэтому компилятор не может выполнять какие-либо строгие оптимизации типов псевдонимов для него.
Вы должны смотреть довольно трудно, чтобы найти стандартную соответствующую реализацию C, которая имеет определенные биты pad реализации в любом из его целочисленных типов. Я сомневаюсь, что вы найдете тот, который реализует любой тип строгого псевдонима оптимизации. Другими словами, компиляторы не используют неопределенное поведение, вызванное доступом к представлению ловушек, чтобы разрешить оптимизацию со строгим псевдонимом, потому что ни один компилятор, реализующий оптимизацию со строгим псевдонимом, не определил никаких представлений ловушек.
Обратите внимание, что было buf
был инициализирован со всеми нулями ('\0'
символов), тогда эта функция не будет иметь неопределенного или определенного реализацией поведения. Представление целых битов с нулем целочисленного типа гарантированно не будет представлением ловушки и будет иметь значение 0.
Теперь для строго соответствующего примера, который использует char
значения для создания гарантированного действительного (возможно, ненулевого) представления long long
значение:
#include <stdio.h>
#include <stdlib.h>
int
main(int argc, char **argv) {
int i;
long long l;
char *buf;
if (argc < 2) {
return 1;
}
buf = malloc(sizeof l);
if (buf == NULL) {
return 1;
}
l = strtoll(argv[1], NULL, 10);
for (i = 0; i < sizeof l; i++) {
buf[i] = ((char *) &l)[i];
}
printf("%lld\n", *(long long *)buf);
return 0;
}
Этот пример не имеет неопределенного поведения и не зависит от выравнивания или представления long long
, Это тот код, для которого было создано исключение типа символа при доступе к объектам. В частности, это означает, что Standard C позволяет вам реализовать свой собственный memcpy
Функция в переносимом C-коде.