Поиск…


замечания

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

  • Алиасирование - это свойство двух указателей a и b , относящихся к одному и тому же объекту, то есть a == b .

  • Эффективный тип объекта данных используется C для определения того, какие операции могут выполняться на этом объекте. В частности, эффективный тип используется для определения того, могут ли два указателя быть похожими друг на друга.

Aliasing может быть проблемой для оптимизации, так как изменение объекта через один указатель, a говорят, может изменить объект , который виден через другой указатель, b . Если ваш компилятор C должен будет предположить, что указатели всегда могут быть псевдонимом друг друга, независимо от их типа и происхождения, многие возможности оптимизации будут потеряны, а многие программы будут работать медленнее.

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

Если не указано иное, два указателя с одинаковым базовым типом могут быть псевдонимом.

Два указателя с различным базовым типом не могут быть псевдонимом, если только один из двух типов не является типом символа.

Здесь базовый тип означает, что мы отбрасываем квалификацию типа, например const , например, если a является double* и b является const double* , компилятор должен обычно предполагать, что изменение *a может изменить *b .

Нарушение второго правила может привести к катастрофическим результатам. Здесь нарушение правила строгого псевдонима означает, что вы представляете компилятору два указателя a и b различного типа, которые на самом деле указывают на один и тот же объект. Тогда компилятор всегда может предположить, что эти два указывают на разные объекты и не будут обновлять его идею *b если вы изменили объект на *a .

Если вы это сделаете, поведение вашей программы станет неопределенным. Таким образом, C ставит довольно жесткие ограничения на конверсии указателей, чтобы помочь вам избежать случайной ситуации.

Если исходный или целевой тип void , все преобразования указателей между указателями с различным базовым типом должны быть явными .

Или, другими словами, им нужен актерский состав , если вы не делаете преобразования, которое просто добавляет к целевому типу квалификатор, такой как const .

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

К символьным типам нельзя обращаться через несимвольные типы.

Если объект определен со статической, потоковой или автоматической продолжительностью хранения, и он имеет тип символа, либо: char , unsigned char или signed char , он может быть недоступен несимвольным типом. В приведенном ниже примере массив char переинтерпретируется как тип int , и поведение не определено при каждом разыменовании указателя int b .

int main( void )
{
    char a[100];
    int* b = ( int* )&a;
    *b = 1;      

    static char c[100];
    b = ( int* )&c;
    *b = 2;

    _Thread_local char d[100];
    b = ( int* )&d;
    *b = 3;
}

Это не определено, поскольку оно нарушает правило «эффективного типа», ни один объект данных, у которого есть эффективный тип, может быть доступен через другой тип, который не является типом символа. Так как другой тип здесь int , это недопустимо.

Даже если размеры выравнивания и указателя будут, как известно, соответствовать, это не освобождает от этого правила, поведение все равно будет неопределенным.

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

Правильный способ достижения той же цели, что и в приведенном выше примере, - это использовать union .

typedef union bufType bufType;
union bufType {
   char c[sizeof(int[25])];
   int i[25];
};

int main( void )
{
    bufType a = { .c = { 0 } }; // reserve a buffer and initialize
    int* b = a.i;      // no cast necessary
    *b = 1;      

    static bufType a = { .c = { 0 } };
    int* b = a.i;
    *b = 2;

    _Thread_local bufType a = { .c = { 0 } };
    int* b = a.i;
    *b = 3;
}

Здесь union гарантирует, что компилятор с самого начала знает, что к буфере можно получить доступ через разные виды. Это также имеет то преимущество, что теперь в буфере есть «вид» ai который уже имеет тип int и не требуется преобразования указателя.

Эффективный тип

Эффективным типом объекта данных является информация последнего типа, связанная с ним, если таковая имеется.

// a normal variable, effective type uint32_t, and this type never changes
uint32_t a = 0.0;

// effective type of *pa is uint32_t, too, simply
// because *pa is the object a
uint32_t* pa = &a;

// the object pointed to by q has no effective type, yet
void* q = malloc(sizeof uint32_t);
// the object pointed to by q still has no effective type,
// because nobody has written to it
uint32_t* qb = q;
// *qb now has effective type uint32_t because a uint32_t value was written
*qb = 37;

// the object pointed to by r has no effective type, yet, although
// it is initialized
void* r = calloc(1, sizeof uint32_t);
// the object pointed to by r still has no effective type,
// because nobody has written to or read from it
uint32_t* rc = r;
// *rc now has effective type uint32_t because a value is read
// from it with that type. The read operation is valid because we used calloc.
// Now the object pointed to by r (which is the same as *rc) has
// gained an effective type, although we didn't change its value.
uint32_t c = *rc;

// the object pointed to by s has no effective type, yet.
void* s = malloc(sizeof uint32_t);
// the object pointed to by s now has effective type uint32_t
// because an uint32_t value is copied into it.
memcpy(s, r, sizeof uint32_t);

Обратите внимание, что для последнего не было необходимости, чтобы у нас даже был указатель uint32_t* на этот объект. Достаточно того факта, что мы скопировали другой объект uint32_t .

Нарушение строгих правил псевдонимов

В следующем коде предположим для простоты, что float и uint32_t имеют одинаковый размер.

void fun(uint32_t* u, float* f) {
    float a = *f
    *u = 22;
    float b = *f;
    print("%g should equal %g\n", a, b);
}

u и f имеют различный базовый тип, и, таким образом, компилятор может предположить, что они указывают на разные объекты. Нет никакой возможности, что *f мог бы измениться между двумя инициализациями a и b , и поэтому компилятор может оптимизировать код для чего-то эквивалентного

void fun(uint32_t* u, float* f) {
    float a = *f
    *u = 22;
    print("%g should equal %g\n", a, a);
}

То есть, вторая операция загрузки *f может быть полностью оптимизирована.

Если мы будем называть эту функцию «нормально»,

 float fval = 4;
 uint32_t uval = 77;
 fun(&uval, &fval);

все идет хорошо, и что-то вроде

4 должно равняться 4

печатается. Но если мы обманываем и передаем один и тот же указатель, после его преобразования,

 float fval = 4;
 uint32_t* up = (uint32_t*)&fval;
 fun(up, &fval);

мы нарушаем строгое правило сглаживания. Тогда поведение становится неопределенным. Вывод может быть таким, как указано выше, если компилятор оптимизировал второй доступ или что-то совершенно другое, и поэтому ваша программа попадает в совершенно ненадежное состояние.

ограничить квалификацию

Если у нас есть два аргумента указателя того же типа, компилятор не может делать никаких предположений и всегда должен будет предположить, что изменение на *e может измениться *f :

void fun(float* e, float* f) {
    float a = *f
    *e = 22;
    float b = *f;
    print("is %g equal to %g?\n", a, b);
}

float fval = 4;
float eval = 77;
 fun(&eval, &fval);

все идет хорошо, и что-то вроде

составляет 4, равный 4?

печатается. Если мы передадим один и тот же указатель, программа по-прежнему будет делать правильные действия и распечатать

составляет 4, равный 22?

Это может оказаться неэффективным, если по какой-то внешней информации мы знаем , что e и f никогда не укажут на один и тот же объект данных. Мы можем отражать это знание, добавляя restrict к параметрам указателя:

void fan(float*restrict e, float*restrict f) {
    float a = *f
    *e = 22;
    float b = *f;
    print("is %g equal to %g?\n", a, b);
}

Тогда компилятор всегда может предположить, что e и f указывают на разные объекты.

Изменение байтов

Когда объект имеет эффективный тип, вы не должны пытаться его модифицировать с помощью указателя другого типа, если только этот тип не является типом char , char , signed char символом или символом unsigned char .

#include <inttypes.h>
#include <stdio.h>

int main(void) {
  uint32_t a = 57;
  // conversion from incompatible types needs a cast !
  unsigned char* ap = (unsigned char*)&a;
  for (size_t i = 0; i < sizeof a; ++i) {
    /* set each byte of a to 42 */
    ap[i] = 42;
  }
  printf("a now has value %" PRIu32 "\n", a);
}

Это действительная программа, которая печатает

a теперь имеет значение 707406378

Это работает, потому что:

  • Доступ осуществляется к отдельным байтам, видимым с типом unsigned char поэтому каждая модификация хорошо определена.
  • Два представления объекта, через a и через *ap , псевдоним, но поскольку ap является указателем на тип символа, правило строгого псевдонима не применяется. Таким образом, компилятор должен предположить, что значение a может быть изменено в цикле for . Модифицированное значение a должно быть построено из байтов, которые были изменены.
  • Тип a , uint32_t не имеет битов заполнения. Все его биты представления подсчитываются для значения, здесь 707406378 , и не может быть ловушечного представления.


Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow