Псевдоним подписи с использованием reinterpret_cast
Возьми следующий код
#include <iostream>
void func() {
int i = 2147483640;
while (i < i + 1)
{
std::cerr << i << '\n';
++i;
}
return;
}
int main() {
func();
}
Этот код явно неверен, так как цикл while может завершиться только в случае переполнения подписанного int i
, то есть UB, и, следовательно, компилятор может, например, оптимизировать его в бесконечный цикл (что Clang делает на -O3
) или выполнить другое всякие прикольные вещи. Теперь мой вопрос: из моего прочтения стандарта C++ типы, эквивалентные до подписи, могут иметь псевдоним (то есть указатели int*
и unsigned*
могут иметь псевдоним). Для того, чтобы сделать некоторую фанки со знаком "обертывание", имеет ли следующее неопределенное поведение или нет?
#include <iostream>
static int safe_inc(int a)
{
++reinterpret_cast<unsigned&>(a);
return a;
}
void func() {
int i = 2147483640;
while (i < safe_inc(i))
{
std::cerr << i << '\n';
++i;
}
return;
}
int main() {
func();
}
Я пробовал приведенный выше код с Clang 8 и GCC 9 на -O3
с -Wall -Wextra -Wpedantic -O3 -fsanitize=address,undefined
аргументы и не получаю ошибок или предупреждений, и цикл завершается после переноса в INT_MIN
.
cppreference.com говорит мне, что
Тип псевдонимов
Всякий раз, когда делается попытка прочитать или изменить сохраненное значение объекта типа DynamicType с помощью glvalue типа AliasedType, поведение не определено, если не выполнено одно из следующих условий:
- AliasedType является (возможно, cv-квалифицированным) подписанным или неподписанным вариантом DynamicType.
что из моего прочтения означает, что для целей псевдонимов типов подпись не учитывается, и код, использующий reinterpret_cast
имеет четко определенную семантику (хотя, в любом случае, он немного глуповат).
Ответы
Ответ 1
Псевдоним здесь вполне законен. См. Http://eel.is/c++draft/expr.prop#basic.lval-11.2:
Если программа пытается получить доступ к сохраненному значению объекта через glvalue, тип которого не похож ([conv.qual]) на один из следующих типов, поведение не определено: 53
(11.1) динамический тип объекта,
(11.2) тип, который является типом со знаком или без знака, соответствующим динамическому типу объекта
Я думаю, также стоит поговорить о реальном вопросе переполнения, который не обязательно требует reinterpret_cast
. Тот же самый эффект может быть достигнут с неявными интегральными преобразованиями
unsigned x = i;
++x;
i = x; // this would serve you just fine.
Этот код будет определяться реализацией до С++ 20, поскольку вы будете конвертировать из значения, которое не может быть представлено целевым типом.
Начиная с С++ 20 этот код будет корректным.
См. Https://en.cppreference.com/w/cpp/language/implicit_conversion
Напомним, что вы также можете начать с беззнакового типа, если хотите семантическое переполнение целочисленного типа.
Ответ 2
Ваш код абсолютно легален, ссылка на cpp - очень хороший источник. Вы можете найти ту же информацию в стандарте [basic.lval]/11
Если программа пытается получить доступ к сохраненному значению объекта через glvalue, тип которого не похож ([conv.qual]) на один из следующих типов, поведение не определено:
-
динамический тип объекта,
-
тип, который является типом со знаком или без знака, соответствующим динамическому типу объекта, [...]
Ответ 3
В C++ 20 я полагаю, что стандарт был обновлен, чтобы допустить дополнение 2s, которое является предположением, которое вы делаете и почему ваш код работает. Без этого предположения это неопределенное поведение.