Ничто в Стандарте не предполагает, что операция, выполняющая запись в объект, должна распознаваться как установка Эффективного типа только в тех случаях, когда операция имеет и другие побочные эффекты (например, изменение структуры битов, хранящихся в этот объект). С другой стороны, компиляторы, которые используют агрессивную оптимизацию на основе типов, по-видимому, не могут распознать возможное изменение эффективного типа объекта как побочный эффект, который должен поддерживаться, даже если запись не будет иметь других наблюдаемых побочных эффектов.
Чтобы понять, что на самом деле говорит правило эффективного типа, я думаю, что необходимо понять, откуда оно взято. Насколько я могу судить, он, по-видимому, основан на отчете о дефектах № 028, более конкретно, на обосновании, используемом для обоснования приведенного в нем заключения. Данное заключение является разумным, но приведенное обоснование абсурдно.
По сути, основная предпосылка предполагает возможность чего-то вроде:
void actOnTwoThings(T1 *p1, T2 *p2)
{
... code that uses p1 and p2
}
...
...in some other function
union {T1 v1; T2 v2; } u;
actOnTwoThings(&u.v1, &u.v2);
Поскольку этот акт записи объединения как одного типа и чтения как другого приводит к поведению, определяемому реализацией, поведение записи одного члена объединения с помощью указателя и чтения другого не полностью определено Стандартом и поэтому должно ( логика DR # 028) должна рассматриваться как неопределенное поведение. Хотя использование p1 и p2 для доступа к одному и тому же хранилищу на самом деле должно рассматриваться как UB во многих сценариях, подобных приведенному выше, логическое обоснование совершенно неверно. Указание того, что действие приводит к определенному для реализации поведению, очень отличается от того, что оно дает неопределенное поведение, особенно в тех случаях, когда стандарт налагает ограничения на то, каким может быть поведение, определяемое реализацией.
Ключевым результатом получения правил типа указателя из поведения объединений является то, что поведение полностью и однозначно определено, без аспектов, определяемых реализацией, если код записывает объединение любое количество раз, используя какие-либо члены в любой последовательности, а затем читает последний член написано. Хотя требование того, чтобы реализации позволили это, блокировало некоторые полезные в противном случае оптимизации, довольно ясно, что правила эффективного типа написаны так, чтобы требовать такого поведения.
Большая проблема, возникающая из-за того, что правила типов основываются на поведении объединений, состоит в том, что действие чтения объединения, использующего один тип, и записи объединения с другим типом не должно рассматриваться как имеющее какие-либо побочные эффекты, если новый битовый шаблон соответствует старому. Поскольку реализация должна будет определять новый битовый шаблон как представляющий значение, которое было записано как новый тип, она также должна определять (идентичный) старый битовый шаблон как представляющий это же значение. Учитывая функцию (предположим, что 'long' и 'long long' одного типа):
long test(long *p1, long long *p2, void *p3)
{
if (*p1)
{
long long temp;
*p2 = 1;
temp = *(long long*)p3;
*(long*)p3 = temp;
}
return *p1;
}
и gcc, и clang решат, что запись с помощью *(long*)p3
не может иметь никакого эффекта, поскольку она просто сохраняет ту же самую битовую комбинацию, которая была прочитана с помощью *(long long*)p3
, что будет истинно, если следующее чтение *p1
собирались обрабатывать в режиме, определяемом реализацией, в случае, если хранилище было записано с помощью *p2
, но это не так, если этот случай рассматривается как UB. К сожалению, поскольку стандарт не соответствует тому, является ли поведение определяемым реализацией или не определено, непоследовательным является вопрос о том, следует ли считать запись побочным эффектом.
С практической точки зрения, когда не используется -fno-strict-aliasing
, gcc и clang следует рассматривать как обработку диалекта C, когда эффективные типы, когда они установлены, становятся постоянными. Они не могут надежно распознать все случаи, когда эффективные типы могут быть изменены, и логика, необходимая для обработки, которая могла бы легко и эффективно обрабатывать многие случаи, о которых авторы gcc давно заявляли, не может быть обработана без оптимизации потрошения.