C Language
Псевдонимы и эффективный тип
Поиск…
замечания
Нарушения правил псевдонимов и нарушения эффективного типа объекта - это две разные вещи, и их не следует смешивать.
Алиасирование - это свойство двух указателей
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
, и не может быть ловушечного представления.