Сегодня я столкнулся с кодом, который выглядит примерно так: И то и другое 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.
Поведение вашей программы реализация определена по двум причинам:
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) Но поскольку вы не знаете, что содержится в этой неинициализированной памяти, поведение при чтении не определено, и это означает, что поведение программы определяется реализацией, поскольку типы символов не могут содержать представления ловушек.
Это зависит от условий.
Если 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-значение беззнакового узкого
тип символа, неопределенное значение заменяет значение
объект, на который ссылается левый операнд.— Если неопределенное значение
узкий тип знака без знака производится путем оценки
выражение инициализации при инициализации объекта без знака
узкий тип символа, этот объект инициализируется неопределенным
значение.
Ответ Вирсавии правильно описывает букву стандарта C ++.
Плохая новость заключается в том, что все современные компиляторы, которые я тестировал (GCC, Clang, MSVC и ICC), игнорируют букву стандарта по этому вопросу. Вместо этого они относятся к лысому утверждению в Приложение J.2 к стандарту C
[поведение не определено, если] значение объекта с автоматической продолжительностью хранения используется, пока оно не определено
как если бы он был на 100% нормативным, как в C, так и в C ++, хотя Приложение J не является нормативным. Это относится к все возможный доступ для чтения к неинициализированному хранилищу, в том числе тщательно выполненный через unsigned char *
и, да, включая доступ на чтение к заполненным байтам.
Более того, если бы вы подали отчет об ошибке, я уверен, что вам скажут, что в той мере, в какой нормативный текст стандарта не соответствует тому, что они делают, это стандарт это неисправно.
хорошо новость в том, что вы будете получать UB только при доступе к дополнительным байтам, если вы осмотреть содержимое байтов заполнения. Копирование их в порядке. В частности, если вы инициализируете все именованные поля структуры POD, будет безопасно скопировать его с помощью назначения структуры и memcpy
, но это будет не быть безопасным, чтобы сравнить его с другой такой структурой, используя memcmp
,