Как безопасно выполнять static_cast между unsigned int и int?

У меня есть 8-символьный string, представляющий шестнадцатеричное число, и мне нужно преобразовать его в int. Это преобразование должно сохранить битовый шаблон для строк "80000000" и выше, т. е. эти числа должны получиться отрицательными. К сожалению, наивное решение:

int hex_str_to_int(const string hexStr)
{    
    stringstream strm;
    strm << hex << hexStr;
    unsigned int val = 0;
    strm >> val;
    return static_cast<int>(val);
}

не работает для моего компилятора, если val > MAX_INT (возвращаемое значение равно 0). Изменение типа val на int также приводит к 0 для больших чисел. Я пробовал несколько разных решений из разных ответов здесь, на SO, и пока не добился успеха.

Вот что я знаю:

  • Я использую компилятор HP C++ на OpenVMS (используя, я полагаю, процессор Itanium).
  • sizeof(int) будет как минимум 4 на каждой архитектуре, на которой будет работать мой код.
  • Преобразование числа > INT_MAX в целое определяется реализацией. На моей машине это обычно приводит к 0, но, что интересно, преобразование из long в int приводит к INT_MAX, когда значение слишком велико.

Это удивительно сложно сделать правильно, по крайней мере, для меня. Кто-нибудь знает о портативном решении этой проблемы?

Обновление:

Изменение static_cast на reinterpret_cast приводит к ошибке компилятора. Комментарий побудил меня попробовать приведение в стиле C: return (int)val в приведенном выше коде, и это сработало. На этой машине. Будет ли это безопасно на других архитектурах?


person Michael Kristofik    schedule 29.09.2011    source источник
comment
Нельзя просто использовать (int)val? Однако изменение типа val на int также приводит к 0.... означает, что проблема может быть связана с >>? (Я понятия не имею, я не использую C++ ;-)   -  person    schedule 29.09.2011
comment
Переполнение целого числа со знаком не определяется реализацией, оно не определено.   -  person derobert    schedule 29.09.2011
comment
@derobert, спасибо, я не был уверен. Я знал, что это нехорошо. Соответственно обновил вопрос.   -  person Michael Kristofik    schedule 29.09.2011
comment
Преобразование из беззнакового в знаковый определяется реализацией, если беззнаковое число не находится в диапазоне знакового типа.   -  person JohnPS    schedule 29.09.2011
comment
@derobert: это не переполнение со знаком, это интегральное преобразование, результат которого определяется реализацией.   -  person ildjarn    schedule 29.09.2011


Ответы (6)


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

int signed_val;
std::memcpy (signed_val, val, sizeof(int));
return signed_val;
person David Hammen    schedule 29.09.2011
comment
Поведение, определяемое реализацией, а не неопределенное. - person ildjarn; 29.09.2011
comment
@ildjarn: Один из широко используемых подходов: return *(int*)(&val); Это поведение, не определяемое реализацией. Это неопределенное поведение. - person David Hammen; 29.09.2011
comment
Ах, это эквивалентно reinterpret_cast, что действительно является UB; Я предположил, что вы имели в виду static_cast в вопросе OP, поведение которого определяется реализацией. - person ildjarn; 29.09.2011
comment
Почему это неопределенное поведение? - person Friedrich; 19.07.2017
comment
Это удивительно быстро с современными компиляторами. - person Emile Cormier; 11.02.2020
comment
C++20 будет иметь bit_cast, который фактически делает то же самое: en.cppreference.com/ w/cpp/числовой/bit_cast - person Emile Cormier; 11.02.2020
comment
@EmileCormier - это довольно быстро даже на самом низком уровне оптимизации, поскольку вызов memcpy не происходит; Я тестировал с несколькими компиляторами. На любом уровне оптимизации, кроме самого низкого, это происходит очень быстро, потому что рабочая переменная (signed_val в моем ответе) оптимизируется. Я подозреваю, что такого рода оптимизации были реализованы задолго до того, как я написал вышеприведенный ответ более 9 лет назад. Правило «как если бы» определенно имело место даже в исходной версии стандарта. Это правило позволяет исключить обращение к memcpy. - person David Hammen; 11.02.2020
comment
@Friedrich - это не определено, потому что стандарт прямо говорит об этом. Это нарушает строгие правила алиасинга C++, которые даже строже, чем строгие правила алиасинга C. Приведение return *(int*)(&val) в стиле C является неопределенным поведением даже в C. - person David Hammen; 11.02.2020
comment
@DavidHammen Только недавно я столкнулся с проблемой, аналогичной OP, и обнаружил, что компиляторы могут оптимизировать memcpy. Спасибо, что указали, что компиляторы давно знали этот трюк. - person Emile Cormier; 11.02.2020

Цитирование стандарта С++ 03, §4.7/3 (интегральные преобразования):

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

Поскольку результат определяется реализацией, по определению невозможно создать действительно переносимое решение.

person ildjarn    schedule 29.09.2011

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

if (val < 0x80000000) // positive values need no conversion
  return val;
if (val == 0x80000000) // Complement-and-addition will overflow, so special case this
  return -0x80000000; // aka INT_MIN
else
  return -(int)(~val + 1);

Это предполагает, что ваши целые числа представлены 32-битным представлением с дополнением до двух (или имеют аналогичный диапазон). Он не зависит от какого-либо неопределенного поведения, связанного с переполнением целого числа со знаком (обратите внимание, что поведение переполнения целого числа unsigned четко определено, хотя и здесь этого не должно происходить!).

Обратите внимание, что если ваши целые числа не 32-битные, все становится сложнее. Возможно, вам придется использовать что-то вроде ~(~0U >> 1) вместо 0x80000000. Кроме того, если ваши целые числа не являются дополнением до двух, у вас могут возникнуть проблемы с переполнением для определенных значений (например, на машине с дополнением до единицы -0x80000000 не может быть представлено в виде 32-разрядного целого числа со знаком). Однако машины без дополнения до двух сегодня очень редки, так что это вряд ли будет проблемой.

person bdonlan    schedule 29.09.2011
comment
Да, я почти уверен, что когда-нибудь этот код будет работать в 64-битной среде. Подобное жесткое кодирование битовых шаблонов, вероятно, не очень хорошая идея. Однако это решение работает на этой машине. - person Michael Kristofik; 29.09.2011
comment
Большинство 64-битных сред используют 32-битные целые числа. В любом случае, вы можете использовать ~(~(unsigned yourinttype)0 >> 1), чтобы найти правильное значение для других типов целых чисел без знака (например, unsigned long long) - person bdonlan; 29.09.2011

Вот еще одно решение, которое сработало для меня:

if (val <= INT_MAX) {
    return static_cast<int>(val);
}
else {
    int ret = static_cast<int>(val & ~INT_MIN);
    return ret | INT_MIN;
}

Если я маскирую старший бит, я избегаю переполнения при приведении. Затем я могу безопасно вернуть его обратно.

person Michael Kristofik    schedule 29.09.2011

C++20 будет иметь std::bit_cast, который дословно копирует биты:

#include <bit>
#include <cassert>
#include <iostream>

int main()
{
    int i = -42;
    auto u = std::bit_cast<unsigned>(i);
    // Prints 4294967254 on two's compliment platforms where int is 32 bits
    std::cout << u << "\n";

    auto roundtripped = std::bit_cast<int>(u);
    assert(roundtripped == i);
    std::cout << roundtripped << "\n"; // Prints -42

    return 0;
}

cppreference показывает пример того, как можно реализовать свои собственные bit_cast с точки зрения memcpy (в примечаниях).

Хотя OpenVMS вряд ли получит поддержку С++ 20 в ближайшее время, я надеюсь, что этот ответ поможет кому-то найти тот же вопрос с помощью поиска в Интернете.

person Emile Cormier    schedule 11.02.2020
comment
Стоит отметить, что подход memcpy соответствует этому ответу stackoverflow.com/a/7602036/46821 - person Michael Kristofik; 13.02.2020

unsigned int u = ~0U;
int s = *reinterpret_cast<int*>(&u); // -1

Наоборот:

int s = -1;
unsigned int u = *reinterpret_cast<unsigned int*>(&s); // all ones
person Papayaved    schedule 02.07.2019