Считает ли предоставление данных эффективный тип побочным эффектом? - PullRequest
0 голосов
/ 25 июня 2018

Предположим, у меня есть кусок динамически распределяемых данных:

void* allocate (size_t n)
{
  void* foo = malloc(n);
  ...
  return foo;
}

Я хочу использовать данные, на которые указывает foo, как специальный тип, type_t. Но я хочу сделать это позже, а не во время распределения. Чтобы присвоить выделенным данным эффективный тип , я могу сделать что-то вроде:

void* allocate (size_t n)
{
  void* foo = malloc(n);
  (void) *(type_t*)foo;
  ...
  return foo
}

Согласно C11 6.5 / 6 этот доступ lvalue должен иметь эффективный тип type_t:

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

Однако строка (void) *(type_t*)foo; не содержит побочных эффектов, поэтому компилятор должен иметь возможность оптимизировать его, и я не ожидал бы, что он сгенерирует какой-либо фактический машинный код.

Мой вопрос: безопасны ли уловки, подобные описанным выше? Считает ли предоставление данных эффективный тип побочным эффектом? Или, оптимизируя код, компилятор также оптимизирует выбор эффективного типа?

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

int* i = allocate(sizeof(int));
*i = something;

Приводит ли это к строгому нарушению псевдонимов UB, как ожидается, или это эффективный тип теперь int?

Ответы [ 2 ]

0 голосов
/ 25 июня 2018

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

Чтобы понять, что на самом деле говорит правило эффективного типа, я думаю, что необходимо понять, откуда оно взято. Насколько я могу судить, он, по-видимому, основан на отчете о дефектах № 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 давно заявляли, не может быть обработана без оптимизации потрошения.

0 голосов
/ 25 июня 2018

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

6,5 / 6

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

...