Прерывает ли разыменование void ** - приведенный тип ** нарушение строгого алиасинга? - PullRequest
0 голосов
/ 01 сентября 2018

Рассмотрим этот искусственный пример:

#include <stddef.h>

static inline void nullify(void **ptr) {
    *ptr = NULL;
}

int main() {
    int i;
    int *p = &i;
    nullify((void **) &p);
    return 0;
}

&p (int **) приводится к void **, который затем разыменовывается. Это нарушает строгие правила наложения имен?

Согласно стандарту :

Объект должен иметь свое сохраненное значение, доступное только через lvalue выражение, которое имеет один из следующих типов:

  • тип, совместимый с эффективным типом объекта,

Таким образом, если void * не считается совместимым с int *, это нарушает строгие правила наложения имен.

Однако это не то, что предлагается в предупреждениях gcc (даже если это ничего не доказывает).

При составлении этого примера:

#include <stddef.h>
void f(int *p) {
    *((float **) &p) = NULL;
}

gcc предупреждает о строгом алиасинге:

$ gcc -c -Wstrict-aliasing -fstrict-aliasing a.c
a.c: In function ‘f’:
a.c:3:7: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
      *((float **) &p) = NULL;
      ~^~~~~~~~~~~~~

Однако с void ** он не предупреждает:

#include <stddef.h>
void f(int *p) {
    *((void **) &p) = NULL;
}

Так ли это в отношении строгих правил наложения имен?

Если это не так, как написать функцию, которая аннулирует любой указатель (например), который не нарушает строгих правил наложения имен?

1 Ответ

0 голосов
/ 01 сентября 2018

Нет общего требования, чтобы реализации использовали одни и те же представления для разных типов указателей. На платформе, которая будет использовать другое представление, например, для int* и char*, не было бы никакого способа поддерживать один тип указателя void*, который мог бы действовать как на int*, так и char* взаимозаменяемо. Хотя реализация, которая может обрабатывать указатели взаимозаменяемо, облегчила бы низкоуровневое программирование на платформах, которые используют совместимые представления, такая возможность не будет поддерживаться на всех платформах. Следовательно, у авторов Стандарта не было никаких оснований требовать поддержки такой функции, а не рассматривать ее как вопрос качества реализации.

Из того, что я могу сказать, качественные компиляторы, такие как icc, которые подходят для низкоуровневого программирования и которые предназначены для платформ, где все указатели имеют одинаковое представление, не будут иметь проблем с такими конструкциями, как:

void resizeOrFail(void **p, size_t newsize)
{
  void *newAddr = realloc(*p, newsize);
  if (!newAddr) fatal_error("Failure to resize");
  *p = newAddr;
}

anyType *thing;

... code chunk #1 that uses thing
   resizeOrFail((void**)&thing, someDesiredSize);
... code chunk #2 that uses thing

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

С другой стороны, если использование было что-то вроде:

void **myptr;    
anyType *thing;

myptr = &thing;
... code chunk #1 that uses thing
*myptr = realloc(*myptr, newSize);
... code chunk #2 that uses thing

тогда даже качественные компиляторы могут не осознавать, что thing может быть затронут между двумя фрагментами кода, которые его используют, поскольку между этими двумя фрагментами нет ссылок на что-либо типа anyType*. На таких компиляторах необходимо написать код примерно так:

myptr = &thing;
... code chunk #1 that uses thing
*(void *volatile*)myptr = realloc(*myptr, newSize);
... code chunk #2 that uses thing

, чтобы сообщить компилятору, что операция на *mtptr делает что-то "странное". Качественные компиляторы, предназначенные для низкоуровневого программирования, будут расценивать это как признак того, что им следует избегать кэширования значения thing во время такой операции, но даже квалификатора volatile будет недостаточно для реализаций, таких как gcc и clang, в оптимизации режимы, предназначенные только для целей, не связанных с низкоуровневым программированием.

Если функция, подобная reallocOrFail, должна работать с режимами компилятора, которые на самом деле не подходят для низкоуровневого программирования, ее можно записать так:

void resizeOrFail(void **p, size_t newsize)
{
  void *newAddr;
  memcpy(&newAddr, p, sizeof newAddr);
  newAddr = realloc(newAddr, newsize);
  if (!newAddr) fatal_error("Failure to resize");
  memcpy(p, &newAddr, sizeof newAddr);
}

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

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

...