Ответ 1
То, как эта библиотека обрабатывает память, не нарушает строгий псевдоним.
Хотя в стандарте C имя не упоминается по имени, строгое псевдонимы в основном означают, что вы не можете получить доступ к объекту одного типа, как если бы это был объект другого типа. Эти правила изложены в параграфах 6 и 7 раздела 6.5:
6 Эффективным типом объекта для доступа к его сохраненному значению является объявленный тип объекта, если таковой имеется. 87) Если значение сохраняется в объекте без объявленного типа через lvalue, имеющий тип, который не является символьным типом, то тип lvalue становится эффективным типом объекта для этот доступ и для последующих доступов, которые не изменяют сохраненный стоимость. Если значение копируется в объект, не имеющий объявленный тип с использованием memcpy или memmove, или копируется как массив тип символа, то эффективный тип измененного объекта для этот доступ и для последующих доступов, которые не изменяют значение эффективный тип объекта, из которого копируется значение, если у него есть один. Для всех других доступов к объекту, не объявленному тип, эффективный тип объекта это просто тип lvalue используется для доступа.
7 Объект должен иметь свое сохраненное значение, доступ к которому осуществляется только через выражение lvalue, имеющее один из следующих типов: 88)
- тип, совместимый с эффективным типом объекта,
- квалифицированная версия типа, совместимого с эффективным типом объекта,
- тип, который является типом со знаком или без знака, соответствующим действующему типу объекта,
- тип, который является типом со знаком или без знака, соответствующим квалифицированной версии действующего типа объекта,
- агрегатный или объединенный тип, который включает в себя один из вышеупомянутых типов среди своих членов (включая, рекурсивно, член субагрегата или автономного союза) или
- тип символа.
87) Выделенные объекты не имеют объявленного типа.
88) Намерение этот список должен указывать те обстоятельства, при которых объект может или не может быть псевдонимом.
Например, следующее нарушает строгий псевдоним:
float x = 3.14;
unsigned int *i = (unsigned int *)&x;
printf("value of x: %f, representation of x: %08x\n", x, *i);
Потому что он пытается прочитать float
, как если бы он был int
.
Работа векторной библиотеки не пытается это сделать.
Давайте посмотрим, как вектор создается библиотекой:
#define vector_grow(vec, count) \
do { \
if(!(vec)) { \
size_t *__p = malloc((count) * sizeof(*(vec)) + (sizeof(size_t) * 2)); \
assert(__p); \
(vec) = (void *)(&__p[2]); \
vector_set_capacity((vec), (count)); \
vector_set_size((vec), 0); \
} else { \
size_t *__p1 = &((size_t *)(vec))[-2]; \
size_t *__p2 = realloc(__p1, ((count) * sizeof(*(vec))+ (sizeof(size_t) * 2))); \
assert(__p2); \
(vec) = (void *)(&__p2[2]); \
vector_set_capacity((vec), (count)); \
} \
} while(0)
И предположим, это называется так:
int *v = NULL;
vector_grow(v, 10);
Поскольку v
имеет значение NULL, часть макроса if
вводится. Он выделяет место для 10 int
плюс 2 size_t
. Сразу после malloc
память, на которую указывает __p
, не имеет типа. Затем он присваивает vec
:
(vec) = (void *)(&__p[2]);
Во-первых, __p
определяется как size_t *
, поэтому &__p[2]
создает указатель на местоположение после 2 объектов типа size_t
, приводит этот указатель к void *
и назначает его vec
. На данный момент ни одна из выделенной памяти еще не имеет типа. Следующий vector_set_capacity
называется:
#define vector_set_capacity(vec, size) \
do { \
if(vec) { \
((size_t *)(vec))[-1] = (size); \
} \
} while(0)
Сначала выполняется приведение vec
к size_t *
, который является исходным типом __p
, и индексирует элемент -1. Это верно, потому что ((size_t *)(vec))[-1]
совпадает с __p[1]
. Теперь здесь записано значение типа size_t
, поэтому байты sizeof(size_t)
, начиная с __p[1]
, содержат объект типа size_t
.
Аналогично для vector_set_size
:
#define vector_set_size(vec, size) \
do { \
if(vec) { \
((size_t *)(vec))[-2] = (size); \
} \
} while(0)
((size_t *)(vec))[-2]
- это то же самое, что и __p[0]
, и запись в него также создает объект типа size_t
.
Итак, теперь память выглядит так:
+--------+----------+---------+
| size_t | size_t | untyped |
+--------+----------+---------+
^ ^ ^
| | |
__p[0] __p[1] __p[2]==vec
Теперь, когда пользователь использует vector_push_back
, он делает это:
vec[vector_size(vec)] = (value);
Это работает так же, как запись в любое выделенное пространство памяти.
Так как доступ к __p[0]
и __p[1]
возможен только через size_t *
, строгого нарушения псевдонимов не существует.
Однако одной проблемой является выравнивание. Память, возвращаемая из malloc
, соответствующим образом выровнена для обработки данных любого типа. Однако при создании другого объекта в этой выделенной памяти без использования struct
эти объекты могут быть неправильно выровнены.
Давайте возьмем в качестве примера систему, в которой int
и size_t
имеют размер 2 байта, и предположим, что блок памяти, возвращенный из malloc
, имеет смещение 0. Теперь мы создадим вектор типа long long
, размером не менее 8 байт. После создания вектора первый size_t
находится со смещением 0, а второй со смещением 2. Это нормально, потому что смещение каждого кратно размеру. Однако это означает, что векторные данные начинаются со смещения 4. Это не кратно 8, поэтому объект типа long long
будет смещен здесь.
Проблема выравнивания может быть решена путем создания объединения max_align_t
и структуры из двух size_t
:
union vector_meta {
struct {
size_t size;
size_t capacity;
}
max_align_t align[2];
};
Тогда vec
будет создан так:
union vector_meta *__p = malloc((count) * sizeof(*(vec)) + (sizeof(union vector_meta)));
assert(__p);
(vec) = (void *)(&__p[1]);
И вы можете получить доступ к размеру и емкости как:
((union vector_meta *)vec)[-1].size
((union vector_meta *)vec)[-1].capacity
Это обеспечивает правильное выравнивание памяти после заголовка метаданных для любого использования и возможность безопасного доступа к полям size
и capacity
.