Undefined поведение при чтении объекта с использованием несимвольного типа при последнем написании с использованием типа символа

Предполагая, что unsigned int не имеет ловушечных представлений, выполните одно или оба из приведенных ниже выражений (A) и (B), спровоцируйте поведение undefined, почему или почему нет, и (особенно, если вы считаете, что один из них хорошо -defined, но другой нет), считаете ли вы, что дефект в стандарте? Меня в первую очередь интересует текущая версия стандарта C (т.е. C2011), но если это отличается в старых версиях стандарта или на С++, я также хотел бы узнать об этом.

(_Alignas используется в этой программе, чтобы исключить любой вопрос UB из-за неадекватного выравнивания. Однако правила, которые я обсуждаю в своей интерпретации, не говорят о выравнивании.)

#include <stdlib.h>
#include <string.h>

int main(void)
{
    unsigned int v1, v2;
    unsigned char _Alignas(unsigned int) b1[sizeof(unsigned int)];
    unsigned char *b2 = malloc(sizeof(unsigned int));

    if (!b2) return 1;

    memset(b1, 0x55, sizeof(unsigned int));
    memset(b2, 0x55, sizeof(unsigned int));

    v1 = *(unsigned int *)b1; /* (A) */
    v2 = *(unsigned int *)b2; /* (B) */

    return !(v1 == v2);
}

Моя интерпретация C2011 заключается в том, что (A) вызывает поведение undefined, но (B) хорошо определен (для хранения неопределенного значения в v2), потому что:

  • memset определяется (§7.24.6.1) для записи в свой первый аргумент as-if через lvalue с типом символа, который разрешен как для b1, так и b2 для специального случая в нижняя часть §6.5p7.

  • Объект b1 имеет объявленный тип unsigned char[n]. Поэтому его эффективный тип для доступа также unsigned char[n] на 6.5p6. Оператор (A) читает b1 через выражение lvalue, тип которого unsigned int, который не является эффективным типом b1 или любым из других исключений в 6.5p7, поэтому поведение undefined.

  • Объект, на который указывает b2, не имеет объявленного типа. Значение, сохраненное в нем (через memset), было (как-если) через lvalue с типом символа, поэтому второй случай 6.5p6 не применяется. Значение не было скопировано нигде, поэтому третий случай 6.5p6 также не применяется. Поэтому эффективным типом объекта является тип lvalue, используемого для доступа, который равен unsigned int, и выполняются правила 6.5p7.

  • Наконец, в соответствии с 6.2.6.1, если unsigned int не имеет ловушечных представлений, операция memset создала представление некоторого неопределенного значения unsigned int в каждом из b1 и b2. Поэтому, если ни (A), ни (B) не провоцируют поведение undefined, то фактические значения в v1 и v2 не определены, но они равны.

Комментарий:

Асимметрия правил "наложения на основе типа" (т.е. 6.5p7), разрешающая доступ к объекту с любым эффективным типом доступа с помощью символьного типа, но не наоборот, является постоянным источником путаницы, Второй случай 6.5p6, кажется, был добавлен специально, чтобы предотвратить его поведение undefined для чтения значения, инициализированного memset (или, если на то пошло, calloc), но, поскольку оно применимо только к объектам без объявленный тип, сам по себе является дополнительным источником путаницы.

Ответ 1

Авторы Стандарта признают в обосновании, что реализация могла быть совместимой, но бесполезной. Поскольку они ожидали, что разработчики будут стремиться сделать их реализации полезными, они не считают необходимым прописывать каждое поведение, которое может потребоваться, чтобы сделать реализацию подходящей для какой-либо конкретной цели.

Стандарт не предъявляет требований к поведению кода, который обращается к выровненному объекту типа символьного массива как к другому типу. Это не означает, что они предполагали, что реализации должны делать что-то иное, чем рассматривать массив как нетипизированное хранилище в случаях, когда код принимает адрес массива один раз, но никогда не обращается к нему напрямую. Фундаментальный характер сглаживания заключается в том, что он требует доступа к элементу двумя разными способами; если объект только когда-либо обращается в один конец, по определению нет псевдонимов. Любая качественная реализация, которая должна быть подходящей для программирования на низком уровне, должна вести себя в полезной форме в случаях, когда char[] используется только как нетипизированное хранилище, независимо от того требует он Стандарт или нет, и трудно представить себе какую-либо полезную цель, которая будет препятствовать такое лечение. Единственной целью, которая могла бы быть обеспечена стандартное мандатное такое поведение, было бы запретить авторам-компиляторам рассматривать отсутствие мандата как само по себе - повод не обрабатывать такой код явным образом.

Ответ 2

На поверхностном экзамене я согласен с вашей оценкой (A - UB, B в порядке) и может предложить конкретное обоснование того, почему это должно быть так (до редактирования включить _Alignas()): Выравнивание.

char[] в стеке может начинаться с любого адреса, будь то допустимое выравнивание для unsigned int или нет. Напротив, malloc() требуется вернуть память, отвечающую самым строгим требованиям к выравниванию любого родного типа на рассматриваемой платформе.

Стандарт, очевидно, не хочет налагать требования выравнивания на char[] выше параметров char, поэтому он должен оставить доступ к нему произвольным доступом как потенциально undefined.