Это неопределенное поведение, чтобы читать и сравнивать байты заполнения типа 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});

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

12

Решение

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


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

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


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


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

9

Другие решения

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

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

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

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

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

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

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

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

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

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

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

По умолчанию инициализировать объект типа T означает:

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

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

— иначе инициализация не выполняется.

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

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

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

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

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

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

или же

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

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

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

1

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

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

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

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

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

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

0
По вопросам рекламы [email protected]