Разрешено ли создавать объекты с одинаковыми смещениями друг от друга в выделенной области памяти? - PullRequest
2 голосов
/ 07 апреля 2020

Это лучше всего объяснить на примере.

typedef struct s_ {
  int a, b;
} s;

int add(s* l, s* r) { return l->a + l->b + r->a + r->b; }

void init(s* v) {
    v->a = 1;
    v->b = 2;
}

int array_like() {
  // allocate enough space for s[2]
  char *p = malloc(2 * sizeof(s));
  s *p1 = (s *)p;
  s *p2 = (s *)(p + sizeof(s));

  init(p1);
  init(p2);
  return add(p1, p2);
}

int array_skip() {
  // allocate enough space for s[3] and init only [0] and [2]
  char *p = malloc(3 * sizeof(s));
  s *p1 = (s *)p;
  s *p2 = (s *)(p + 2 * sizeof(s));

  init(p1);
  init(p2);
  return add(p1, p2);
}

int half_gap() {
  // allocate enough space for 2.5 s objects and lay them
  // out like [s1][gap of alignof(s) bytes][s2]
  char *p = malloc(2 * sizeof(s) + _Alignof(s));
  s *p1 = (s *)p;
  s *p2 = (s *)(p + sizeof(s) + _Alignof(s));

  init(p1);
  init(p2);
  return add(p1, p2);
}

Для конкретности рассмотрим типичную платформу, где sizeof(s) == 8 и alignof(s) == 4 - хотя вопрос должен в равной степени применяться к платформам с разными значениями.

Последние три функции, array_like, array_skip и half_gap, все выполняют аналогичную функцию: они создают два объекта типа s (struct, содержащих два int с) внутри хранилища, выделенного malloc. Все три размещают первый s объект в начале хранилища. Они отличаются только тем, где размещают второй объект:

  • array_like размещает его непосредственно после первого объекта, то есть со смещением 8, поэтому для указателя s* p на начало региона объекты будут в p[0] и p[1].
  • array_skip помещает его sizeof(s) байт после конца первого объекта, то есть со смещением 16, поэтому для указателя s* p на начало области, в которой объекты будут находиться в p[0] и p[2].
  • half_gap помещает его _Alignof(s) байтов после конца первого объекта или 4 байта после первого объекта, т.е. 12 байт от начала хранилища. Обратите внимание, что этот объект по-прежнему правильно выровнен на границе 4 байта, но он не смещен на целое число на sizeof(s) байтов от первого. Вы не можете express определить местоположение второго объекта с массивными обозначениями, как в первых двух случаях.

После этого каждая функция записывает оба члена обоих объектов, а затем читает от них.

Какие из этих функций являются законными C11, и гарантированно ли все законные функции возвращают ожидаемое значение 6?

1 Ответ

3 голосов
/ 07 апреля 2020

Код в вашем вопросе правильный. Приведение указателя определяется тогда и только тогда, когда оно правильно выровнено для типа приведения.

(Сноска. Я видел, как люди утверждают, что 6.3.2.3/7 не указывает, что результат преобразования указывает на один и тот же байт в памяти - однако, если этот аргумент принят, то malloc непригодный для использования, поскольку он не гарантирует преобразование void * в любой тип для будущих точек использования в выделенный блок. Поэтому я не считаю этот аргумент действительным).

Из-за необходимости, чтобы массивы не имели дополняя элементы, мы можем заключить, что sizeof(T) должно быть кратным _Alignof(T), и поэтому все ваши целочисленные выражения кратны _Alignof(T), и, следовательно, все рассматриваемые указатели правильно выровнены.

Что касается терминологии, объект означает «область хранения» в C. Таким образом, все пространство, выделенное mallo c, является объектом, как и любое смежное его подмножество.

Использование оператора присваивания изменяет объект, а не создает его. Когда вы используете оператор присваивания на малло c d, он устанавливает эффективный тип записанных байтов. C11 6.5 / 6 (aka. Строгое правило алиасинга) определяет значение «эффективного типа».


Есть еще одна деталь. Если ваша функция init выглядит следующим образом:

void init(s* v) {
    s a = { .a = 1, .b = 2 };
    *v = a;
}

, тогда это будет конец истории, значение типа s записывается в местоположение. Но в стандарте неясно, какой эффективный тип установлен на v->a = 1;. Наиболее распространенная интерпретация стандарта состоит в том, что v->a = 1 означает (*v).a = 1;, и это назначение также имеет «побочный эффект» установки эффективного типа для всего объекта *v.

Эта интерпретация позволяет TBAA для функции, подобной void f(s *ps, t* pt) (где s - структура без членов типа t), предполагать, что *ps и *pt не пересекаются.

Я уверен, что в этом последнем пункте уже есть несколько вопросов о SO

...