Какова семантика перекрывающихся объектов в C? - PullRequest
25 голосов
/ 07 апреля 2020

Рассмотрим следующую структуру:

struct s {
  int a, b;
};

Обычно 1 , эта структура будет иметь размер 8 и выравнивание 4.

Что если мы создадим два struct s объекты (точнее, мы записываем в выделенное хранилище два таких объекта), причем второй объект перекрывает первый?

char *storage = malloc(3 * sizeof(struct s));
struct s *o1 = (struct s *)storage; // offset 0
struct s *o2 = (struct s *)(storage + alignof(struct s)); // offset 4

// now, o2 points half way into o1
*o1 = (struct s){1, 2};
*o2 = (struct s){3, 4};

printf("o2.a=%d\n", o2->a);
printf("o2.b=%d\n", o2->b);
printf("o1.a=%d\n", o1->a);
printf("o1.b=%d\n", o1->b);

Что-нибудь в этой программе неопределенное поведение? Если так, то где это становится неопределенным? Если это не UB, гарантированно всегда печатается следующее:

o2.a=3
o2.b=4
o1.a=1
o1.b=3

В частности, я хочу знать, что происходит с объектом, на который указывает o1, когда o2, который перекрывает его , написано. Разрешено ли по-прежнему иметь доступ к неблокированной части (o1->a)? Является ли доступ к закрытой части o1->b таким же, как и доступ к o2->a?

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

Изменится ли что-нибудь, если вторая запись будет другого типа? Если участники скажут int и short, а не два int с?

Вот Годболт , если вы хотите поиграть с ним там.


1 Этот ответ относится к платформам, где это не так: например, некоторые могут иметь размер 4 и выравнивание 2. На платформе, где размер и выравнивание были одинаковыми, этот вопрос не будет не применяется, поскольку выровненные, перекрывающиеся объекты были бы невозможны, но я не уверен, существует ли какая-либо подобная платформа.

1 Ответ

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

В основном это все серые области в стандарте; строгое правило псевдонимов определяет основные случаи c и оставляет читателя (и поставщиков компиляторов) для заполнения деталей.

Были попытки написать лучшее правило, но пока они не привели ни к какому нормативный текст, и я не уверен, каково его состояние для C2x.

Как упоминалось в моем ответе на ваш предыдущий вопрос, наиболее распространенная интерпретация заключается в том, что p->q означает (*p).q, а эффективный тип применяется ко всем *p, хотя мы тогда go применяем .q.

В этой интерпретации printf("o1.a=%d\n", o1->a); приведет к неопределенному поведению, поскольку эффективный тип местоположения *o1 не s (поскольку его часть была перезаписана).

Обоснование этой интерпретации можно увидеть в такой функции:

void f(s* s1, s* s2)
{
    s2->a = 5;
    s1->b = 6;
    printf("%d\n", s2->a);
}

При такой интерпретации последняя строка может быть оптимизирована до puts("5");, но без нее компилятору придется учитывать что вызов функции мог быть f(o1, o2); и, следовательно, потерять все преимущества, которые якобы предусмотрены строгим правилом псевдонимов.

Аналогичный аргумент применяется к двум несвязанным типам структур, которые оба имеют элемент int с различным смещением.

...