Это поведение undefined для чтения и сравнения байтов заполнения типа POD?

Сегодня я столкнулся с некоторым кодом, который примерно похож на следующий фрагмент. И valgrind, и UndefinedBehaviorSanitizer обнаружены чтения неинициализированных данных.

template <typename T>
void foo(const T& x)
{
    static_assert(std::is_pod_v<T> && sizeof(T) > 1);
    auto p = reinterpret_cast<const char*>(&x);

    std::size_t i = 1; 
    for(; i < sizeof(T); ++i)
    {
        if(p[i] != p[0]) { break; }
    }

    // ...
}

Вышеупомянутые инструменты жаловались на сравнение p[i] != p[0], когда объект, содержащий байты заполнения, был передан в foo. Пример:

struct obj { char c; int* i; };
foo(obj{'b', nullptr});

Это поведение undefined для чтения байтов заполнения из типа POD и сравнения их с чем-то еще? Я не смог найти окончательный ответ ни в стандарте, ни в StackOverflow.

Ответ 1

Поведение вашей программы - это реализация, определенная по двум значениям:


1) До С++ 14: из-за возможности 1-символьного типа signed для уровня signed, вы можете вернуть неожиданный результат из-за сравнения +0 и -0.

По-настоящему водонепроницаемым способом будет использовать указатель const unsigned char*. Это устраняет любые проблемы с отмененным (от С++ 14) 1 дополнением или знаковым значением char.


Так как (i) у вас есть память, (ii) вы берете указатель на x и (iii) unsigned char не может содержать ловушечное представление, (iv) char, unsigned char, и signed char освобождается от правил строгого сглаживания, поведение при использовании const unsigned char* для чтения неинициализированной памяти прекрасно определено.


2) Но так как вы не знаете, что содержится в этой неинициализированной памяти, поведение при чтении не указано, и это означает, что поведение программы является реализацией, так как типы char не могут содержать ловушечные представления.

Ответ 2

Это зависит от условий.

Если x инициализируется нулем, то заполнение имеет нулевые биты, поэтому этот случай четко определен (8.5/6 из С++ 14):

Для нулевой инициализации объекта или ссылки типа T означает:

- если T - скалярный тип (3.9), объект инициализируется значением полученных путем преобразования целочисленного литерала

0 (ноль) до T; 105

- если T является (возможно, cv-quali-fi) типом неединичного класса, каждый нестатический элемент данных и каждый базовый класс

подобъект инициализируется нулем и заполнение инициализируется нулевыми битами;

- если T является (возможно, cv-qualified) объединенным типом, объекты сначала нестатический именованный элемент данных равен нулю -

инициализировано и заполнение инициализируется нулевыми битами;

- если T - тип массива, каждый элемент инициализируется нулем; - если T является ссылочный тип, инициализация не выполняется.

Однако, если x инициализируется по умолчанию, то заполнение не указывается, поэтому оно имеет неопределенное значение (вывод о том, что здесь нет упоминания о заполнении) (8.5/7):

Для инициализации объекта типа T по умолчанию:

- если T является (возможно, cv-quali-fi) типом класса (раздел 9), значение по умолчанию конструктор (12.1) для T называется (и инициализация плохо сформированный, если T не имеет конструктора по умолчанию или разрешения перегрузки (13.3) приводит к двусмысленности или в функции, которая удалена или недоступным из контекста инициализации);

- если T - тип массива, каждый элемент инициализируется по умолчанию;

- в противном случае инициализация не выполняется.

И сравнение неопределенных значений UB для этого случая, поскольку ни одно из упомянутых исключений не применяется, поскольку вы сравниваете неопределенное значение с чем-то (8.5/12):

Если для объекта не задан инициализатор, объект по умолчанию инициализируется. При хранении для объекта с автоматическим или динамическая длительность хранения, объект имеет неопределенный значение, и если для объекта не выполняется инициализация, это объект сохраняет неопределенное значение до тех пор, пока это значение не будет заменено (5.17). [Примечание. Объекты со статической или длительностью хранения потоков нулевой инициализации, см. 3.6.2. - end note] Если неопределенное значение равно полученное путем оценки, поведение не определено, за исключением случаев, когда следующие случаи:

- Если неопределенное значение беззнакового узкого символьного типа (3.9.1) производится путем оценки:

......- второй или третий операнд условного выражения (5.16),

......- правый операнд выражения с запятой (5.18),

...... - операнд отливки или преобразование в неподписанный тип узкого символа (4.7, 5.2.3, 5.2.9, 5.4),

или

......- выражение с отброшенным значением (п. 5), то результат операция является неопределенным значением.

- Если неопределенное значение беззнакового узкого символьного типа полученных путем оценки правильного операнда простого назначения оператор (5.17), первый операнд которого является lзначением беззнакового узкого тип символа, неопределенное значение заменяет значение объект, на который ссылается левый операнд.

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

Ответ 3

Ответ на вопрос о ваннхебе правильно описывает букву стандарта С++.

Плохая новость в том, что все современные компиляторы, которые я тестировал (GCC, Clang, MSVC и ICC), все игнорируют букву стандарта по этому вопросу. Вместо этого они рассматривают лысину в Приложение J.2 к стандарту C

[поведение undefined если] используется значение объекта с автоматической продолжительностью хранения, пока оно неопределено

как если бы он был на 100% нормативным, как на C, так и на С++, хотя Приложение J не является нормативным. Это относится ко всем возможным обращению к доступу к неинициализированному хранилищу, в том числе к тем, которые тщательно выполняются с помощью unsigned char *, и, да, включая чтение доступа к байтам заполнения.

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

Хорошей новостью является то, что вы будете подвергать UB только при доступе к байтам заполнения, если вы проверите содержимое байтов заполнения. Копирование их в порядке. В частности, если вы инициализируете все именованные поля структуры POD, можно будет скопировать ее по назначению структуры и memcpy, но небезопасно сравнивать ее с другой такой структурой, используя memcmp.