Какова семантика перекрывающихся объектов в C?

25

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

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

BeeOnRope
источник
2
Я почти уверен, что это UB, но я позволю языковому юристу предоставить главу и стих.
Бармар
Я думаю, что компилятор C в старых векторных системах Cray принудительно выровнял выравнивание и размер с моделью ILP64 и принудительным 64-разрядным выравниванием (адреса - это 64-разрядные слова - без байтовой адресации). Конечно, это породило множество других проблем ....
Джон Д. Маккальпин

Ответы:

15

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

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

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

М.М.
источник
1
С f(s* s1, s* s2), без restrict, компилятор не может предполагать s1и s2являются разными указателями. Я думаю , опять же, без этого restrict, он даже не может предположить, что они не частично перекрываются. IAC, я не вижу, что по f()аналогии озабоченность OP хорошо продемонстрирована . Удачи неослабный. УФ для первой половины.
chux - Восстановить Монику
@ chux-ReinstateMonica без ограничений s1 == s2будет разрешено, но не частичное перекрытие. (Оптимизация в моем примере кода все еще может быть выполнена, если s1 == s2)
ММ
@ chux-ReinstateMonica, вы могли бы также рассмотреть ту же проблему с просто intвместо структур (и системы с _Alignof(int) < sizeof(int)).
ММ
3
Статус такого рода вопроса относительно эффективного типа для C2x является в значительной степени открытым и все еще подлежит обсуждению в исследовательской комиссии. Будьте осторожны, утверждая эквивалентность p->qи (*p).q. Это может быть верно для интерпретации типа, как вы заявляете, но это не так с эксплуатационной точки зрения. Для одновременного доступа к той же структуре важно, чтобы доступ члена не подразумевал доступ любого другого члена.
Йенс Гастедт
Строгое правило псевдонимов касается доступа . Левое выражение в E1.E2выражении не выполняет доступ (я имею в виду целое E1выражение. Некоторые из его подвыражений могут выполнять доступ. Т.е. если E1есть (*p), то чтение значения указателя при вычислении pявляется доступом, но оценка *pили (*p)не выполняет какой-либо доступ). Строгое правило псевдонимов не применяется в случае отсутствия доступа.
Языковой адвокат