Псевдоним подписи с использованием 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, которое является предположением, которое вы делаете и почему ваш код работает. Без этого предположения это неопределенное поведение.