Эффективное приведение без знака к знаку, избегающее поведения, определяемого реализацией
я хочу определить функцию, которая принимает unsigned int
в качестве аргумента и возвращает int
конгруэнтный по модулю UINT_MAX+1 к аргументу.
первая попытка может выглядеть так:
int unsigned_to_signed(unsigned n)
{
return static_cast<int>(n);
}
но, как знает любой юрист языка, приведение от unsigned к signed для значений больше, чем INT_MAX, определяется реализацией.
я хочу реализовать это так ,что (а) он полагается только на поведение, предписанное спецификацией; и (б) он компилируется в no-op on любая современная машина и оптимизирующий компилятор.
что касается странных машин... Если нет подписанного int конгруэнтного по модулю UINT_MAX+1 к unsigned int, допустим, я хочу создать исключение. Если есть более одного (я не уверен, что это возможно), скажем, я хочу самый большой.
ОК, вторая попытка:
int unsigned_to_signed(unsigned n)
{
int int_n = static_cast<int>(n);
if (n == static_cast<unsigned>(int_n))
return int_n;
// else do something long and complicated
}
я не очень забочусь об эффективности, когда я не нахожусь на типичной системе двойки-дополнения, так как по моему скромному мнению это едва ли. И если мой код станет узким местом на вездесущих знаковых системах 2050 года, ну, я уверен, что кто-то может понять это и оптимизировать его.
теперь эта вторая попытка довольно близка к тому, что я хочу. Хотя актерский состав до int
определяется реализация для некоторых входов, приведение обратно к unsigned
стандарт гарантирует сохранение значения по модулю UINT_MAX+1. Таким образом, условное условие проверяет именно то, что я хочу, и оно будет компилироваться в ничто на любом система, с которой я, вероятно, столкнусь.
однако... Я все еще бросаю в int
без предварительной проверки, будет ли он вызывать определенное реализацией поведение. По какой-то гипотетической системе в 2050 году это может сделать кто-знает-что. Скажем так, я хочу избежать этого.
вопрос: как должна выглядеть моя "третья попытка"?
Итак, я хочу:
- приведение от unsigned int к signed int
- сохранить значение мод UINT_MAX+1
- вызывать только стандартное поведение
- компиляция в no-op на типичной машине с двумя дополнениями с оптимизирующим компилятором
[обновление]
позвольте мне привести пример, чтобы показать, почему это не тривиальный вопрос.
рассмотрим гипотетическую реализацию C++ со следующими свойствами:
-
sizeof(int)
равна 4 -
sizeof(unsigned)
равна 4 -
INT_MAX
равна 32767 -
INT_MIN
равна -232 + 32768 -
UINT_MAX
равна 232 - 1 - арифметика на
int
по модулю 232 (в границахINT_MIN
черезINT_MAX
) -
std::numeric_limits<int>::is_modulo
истинно - литье без знака
n
to int сохраняет значение для 0 ноль иначе
об этой гипотетической реализации, есть ровно один int
значение congruent (mod UINT_MAX+1) для каждого unsigned
значение. Так что мой вопрос будет четко определен.
я утверждаю, что эта гипотетическая реализация C++ полностью соответствует спецификациям C++98, C++03 и C++11. Признаюсь, я не запомнил каждое слово из них... Но я считаю, что внимательно прочитал соответствующие разделы. Поэтому, если вы хотите, чтобы я принял ваш ответ, вы либо должны (а) привести спецификацию, которая исключает эту гипотетическую реализацию или (б) обращаться с ним правильно.
действительно, правильный ответ должен обрабатывать гипотетическая реализация, разрешенная стандартом. Это то, что по определению означает" вызывать только стандартное поведение".
Кстати, обратите внимание, что std::numeric_limits<int>::is_modulo
совершенно бесполезно здесь по нескольким причинам. Во-первых, это может быть true
даже если приведения без знака к знаку не работают для больших значений без знака. Во-вторых, это может быть true
даже на системы одного дополнения или знака величины, если арифметика просто по модулю всего целого диапазона. И так далее. Если ваш ответ зависит от is_modulo
, это неправильно.
[обновление 2]
ответ hvd научил меня кое-чему: моя гипотетическая реализация C++ для целых чисел не разрешено современным C. стандарты C99 и C11 очень специфичны в отношении представления целых чисел со знаком; действительно, они разрешают только двойки-дополнение, одни-дополняющие, а другие-знаковые (пункт 2 раздела 6.2.6.2);).
но C++ - это не C. Как оказалось, этот факт лежит в самом сердце моего вопроса.
оригинальный стандарт C++98 был основан на гораздо более старом C89, который говорит (раздел 3.1.2.5):
для каждого из целочисленных типов со знаком есть соответствующий (но другой) беззнаковый целочисленный тип (обозначается ключевым словом unsigned), который использует тот же объем памяти (включая знак информация) и имеет те же требования к выравниванию. Ассортимент неотрицательные значения целочисленного типа со знаком-это поддиапазон соответствующий беззнаковый целочисленный тип, и представление одно и то же значение в каждом типе одинаково.
C89 ничего не говорит о том, чтобы иметь только один знаковый бит или разрешать только двоек-дополнение/единицы-дополнение/знак-величина.
стандарт C++98 принял этот язык почти дословно (раздел 3.9.1 пункт (3)):
для каждого из целочисленных типов со знаком существует соответствующий (но по-другому) целочисленный тип без знака: "
unsigned char
","unsigned short int
","unsigned int
" и "unsigned long int
", каждый из который занимает одинаковый объем памяти и имеет одинаковое выравнивание требования (3.9), как и соответствующий знаковый целочисленный тип ; что есть, друг целое число со знаком тип имеет то же представление объекта, что и соответствующий целое число без знака тип. Диапазон неотрицательных значения целочисленного типа со знаком-это поддиапазон соответствующего целочисленный тип без знака и представление значений каждого из них соответствующий подписанный / неподписанный тип должен быть одинаковым.
стандарт C++03 использует практически идентичный язык, как и C++11.
ни одна стандартная спецификация C++ не ограничивает свои целочисленные представления со знаком любой спецификацией C, насколько я могу судить. А там ничего нет обязательный один знак бит или что-нибудь в этом роде. Все это говорит о том, что неотрицательных целые числа со знаком должны быть поддиапазоном соответствующего без знака.
Итак, я снова утверждаю, что INT_MAX=32767 с INT_MIN=-232+32768 допускается. Если ваш ответ предполагает иное, он неверен, если вы не цитируете C++ стандарт доказывает, что я ошибаюсь.
8 ответов:
расширение ответа пользователя 71404:
int f(unsigned x) { if (x <= INT_MAX) return static_cast<int>(x); if (x >= INT_MIN) return static_cast<int>(x - INT_MIN) + INT_MIN; throw x; // Or whatever else you like }
если
x >= INT_MIN
(имейте в виду правила продвижения,INT_MIN
преобразуется к видуunsigned
), тоx - INT_MIN <= INT_MAX
, так что это не переполнение.если это не очевидно, взгляните на заявление "если
x >= -4u
, потомx + 4 <= 3
."и имейте в виду, чтоINT_MAX
будет равно как минимум математическому значению-INT_MIN-1.на наиболее распространенных системах, где
!(x <= INT_MAX)
подразумеваетx >= INT_MIN
оптимизатор должен быть в состоянии (и на моей системе, в состоянии) удалить вторую проверку, определить, что дваreturn
операторы могут быть скомпилированы в тот же код, и удалить первую проверку тоже. Сгенерированный список сборок:__Z1fj: LFB6: .cfi_startproc movl 4(%esp), %eax ret .cfi_endproc
гипотетическая реализация в вашем вопросе:
- INT_MAX равно 32767
- INT_MIN равно -232 + 32768
не возможно, поэтому не требует специального рассмотрения.
INT_MIN
будет равно либо-INT_MAX
или-INT_MAX - 1
. Это следует из представления c целочисленных типов (6.2.6.2), которое требуетn
биты должны быть битами значения, один бит должен быть битом знака и позволяет только одно представление ловушки (не включая представления, которые являются недопустимыми из-за битов заполнения), а именно тот, который в противном случае представлял бы отрицательный ноль /-INT_MAX - 1
. C++ не допускает никаких целочисленных представлений за пределами того, что C позволяет.обновление: компилятор Microsoft, по-видимому, не замечает этого
x > 10
иx >= 11
тест то же самое. Он генерирует только нужный код, еслиx >= INT_MIN
заменяетсяx > INT_MIN - 1u
, которые он может обнаружить как отрицаниеx <= INT_MAX
(на этой платформе).[обновление от questioner (Nemo), уточняя наше обсуждение ниже]
теперь я считаю, что этот ответ работает во всех случаях, но по сложным причинам. Я вероятно, я назначу награду за это решение, но я хочу захватить все кровавые детали, если кто-то заботится.
начнем с C++11, раздел 18.3.3:
таблица 31 описывает заголовок
<climits>
....
содержимое совпадает с заголовком стандартной библиотеки C
<limits.h>
.здесь "стандарт C" означает C99, спецификация которого сильно ограничивает представление подписанного целые. Они похожи на целые числа без знака, но с одним битом, предназначенным для "знака", и нулем или более битов, предназначенных для "заполнения". Биты заполнения не вносят вклад в значение целого числа, а знаковый бит вносит вклад только как двойки-дополнение, единицы-дополнение или знак-величина.
так как C++11 наследует
<climits>
макросы из C99, INT_MIN-это либо-INT_MAX, либо-INT_MAX-1, и код hvd гарантированно работает. (Обратите внимание, что из-за заполнения INT_MAX может быть намного меньше чем UINT_MAX/2... Но благодаря тому, как работает signed - >unsigned casts, этот ответ Отлично справляется.)C++03 / C++98 сложнее. Он использует ту же формулировку, чтобы наследовать
<climits>
от "Standard C", но теперь" Standard C " означает C89/C90.все эти -- C++98, C++03, C89/C90 -- имеют формулировку, которую я даю в своем вопросе, но также включают это (c++03 раздел 3.9.1 пункт 7):
представления интегральных типов должны определять значения путем использование чистая двоичная система счисления.(44) [пример: этот международный Стандартные разрешения дополняют 2, дополняют 1 и подписывают величину представления для интегральных типов.]
сноска (44) определяет "чистую двоичную систему счисления":
позиционное представление для целых чисел используются двоичные цифры 0 и 1, в котором значения, представленные последовательными битами, являются аддитивные, начинаются с 1 и умножаются путем последовательного интегрального мощность 2, за исключением разве что бита с самой высокой позицией.
что интересно в этой формулировке, так это то, что она противоречит самой себе, потому что определение "чистой двоичной системы счисления" не допускает представления знака/величины! Это позволяет высокому биту иметь, скажем, значение -2n-1 (twos дополняют) или(2n-1-1) (единиц комплемента). Но нет никакого значения для высокого бита, который приводит к знак/величина.
в любом случае, моя "гипотетическая реализация" не квалифицируется как "чистый двоичный файл" в соответствии с этим определением, поэтому она исключена.
однако тот факт, что высокий бит является специальным, означает, что мы можем представить, что он вносит какое-либо значение вообще: небольшое положительное значение, огромное положительное значение, небольшое отрицательное значение или огромное отрицательное значение. (Если знаковый бит может внести свой вклад - (2n-1-1), почему бы и нет -(2n-1-2)? так далее.)
так, давайте представим себе целочисленное представление со знаком, которое присваивает дурацкое значение биту "знак".
небольшое положительное значение для знакового бита приведет к положительному диапазону для
int
(возможно, какunsigned
), и код hvd обрабатывает это просто отлично.огромное положительное значение для знакового бита приведет в
int
имея максимум больше, чемunsigned
, что запрещено.огромное отрицательное значение для знакового бита приведет к
int
представляющий несмежный диапазон значений и другие формулировки в правилах спецификации, которые выходят.наконец, как насчет знакового бита, который вносит небольшую отрицательную величину? Можем ли мы иметь 1 в "знаковом бите" внести, скажем, -37 в значение int? Так что тогда INT_MAX будет (скажем) 231-1 и INT_MIN будет -37?
это приведет к тому, что некоторые числа будут иметь два представления... Но один-дополнение дает два представления ноль, и это разрешено в соответствии с "примером". Нигде спецификация не говорит, что ноль-это только целое число, которое может иметь два представления. Поэтому я думаю, что это новое гипотетическое разрешено спецификацией.
действительно, любое отрицательное значение от -1 до
-INT_MAX-1
представляется допустимым в качестве значения для "знакового бита", но не меньше (чтобы диапазон не был непрерывным). Другими словами,INT_MIN
может быть что угодно от-INT_MAX-1
к-1.теперь, знаешь что? Для второго приведения в коде hvd, чтобы избежать определяемого реализацией поведения, нам просто нужно
x - (unsigned)INT_MIN
меньше или равноINT_MAX
. Мы только что показалиINT_MIN
по крайней мере-INT_MAX-1
. Очевидно,x
наUINT_MAX
. Приведение отрицательного числа к unsigned-это то же самое, что добавлениеUINT_MAX+1
. Сложите все вместе:x - (unsigned)INT_MIN <= INT_MAX
если и только если
UINT_MAX - (INT_MIN + UINT_MAX + 1) <= INT_MAX -INT_MIN-1 <= INT_MAX -INT_MIN <= INT_MAX+1 INT_MIN >= -INT_MAX-1
это последнее, что мы только что показали, так что даже в этом извращенном случае код на самом деле завод.
это исчерпывает все возможности, таким образом, заканчивая это чрезвычайно академическое упражнение.
итог: существует некоторое серьезно недоопределенное поведение для целых чисел со знаком в C89/C90, которые были унаследованы C++98/C++03. Он исправлен в C99, и C++11 косвенно наследует исправление путем включения
<limits.h>
от C99. Но даже C++11 сохраняет самопротиворечивую формулировку "чистого двоичного представления"...
этот код зависит только от поведения, предписанного спецификацией, поэтому требование (a) легко выполняется:
int unsigned_to_signed(unsigned n) { int result = INT_MAX; if (n > INT_MAX && n < INT_MIN) throw runtime_error("no signed int for this number"); for (unsigned i = INT_MAX; i != n; --i) --result; return result; }
это не так просто с требованием (b). Это компилируется в no-op с gcc 4.6.3 (- Os,- O2,- O3) и с clang 3.0 (- Os,- O,- O2, - O3). Intel 12.1.0 отказывается оптимизировать это. И у меня нет информации о Visual С.
вы можете явно сказать компилятору, что вы хотите сделать:
int unsigned_to_signed(unsigned n) { if (n > INT_MAX) { if (n <= UINT_MAX + INT_MIN) { throw "no result"; } return static_cast<int>(n + INT_MIN) - (UINT_MAX + INT_MIN + 1); } else { return static_cast<int>(n); } }
составляет с
gcc 4.7.2
наx86_64-linux
(g++ -O -S test.cpp
) к_Z18unsigned_to_signedj: movl %edi, %eax ret
если
x
- это наш вклад...если
x > INT_MAX
мы хотим найти постояннуюk
такое, что0
x - k*INT_MAX INT_MAX.это просто...
unsigned int k = x / INT_MAX;
. Тогда пустьunsigned int x2 = x - k*INT_MAX;
теперь мы можем литой
x2
доint
безопасно. Пустьint x3 = static_cast<int>(x2);
теперь мы хотим вычесть что-то вроде
UINT_MAX - k * INT_MAX + 1
Сx3
, еслиk > 0
.теперь, на системе дополнения 2s, пока
x > INT_MAX
, это работает к:unsigned int k = x / INT_MAX; x -= k*INT_MAX; int r = int(x); r += k*INT_MAX; r -= UINT_MAX+1;
отметим, что
UINT_MAX+1
равен нулю в C++ гарантировано, преобразование в int было noop, и мы вычиталиk*INT_MAX
затем добавил его обратно на "то же значение". Так что приемлемый оптимизатор должен быть в состоянии стереть все это дурачество!это оставляет проблему
x > INT_MAX
или нет. Ну, мы создаем 2 ветки, одна сx > INT_MAX
, и один без. Тот, у кого нет, делает прямое приведение, которое компилятор оптимизирует до noop. Один ... делает НУП после оптимизатор выполнен. Умный оптимизатор реализует обе ветви к одному и тому же, и отбрасывает ветвь.вопросы: если
UINT_MAX
действительно большой относительноINT_MAX
выше может не работать. Я предполагаю, чтоk*INT_MAX <= UINT_MAX+1
неявно.мы могли бы, вероятно, атаковать это с некоторыми перечислениями, как:
enum { divisor = UINT_MAX/INT_MAX, remainder = UINT_MAX-divisor*INT_MAX };
которые работают на 2 и 1 в системе дополнения 2s, я считаю (мы гарантируем, что эта математика будет работать? Это сложно...), и делать логику на основе на них, которые легко оптимизируются на системах дополнения non-2s...
это также открывает случай исключение. Это возможно только в том случае, если UINT_MAX намного больше (INT_MIN-INT_MAX), поэтому вы можете поместить свой код исключения в блок if, задавая точно такой вопрос, и это не замедлит вас в традиционной системе.
я не знаю, как строить эти константы времени компиляции, чтобы правильно справиться с этим.
мои деньги на использование memcpy. Любой приличный компилятор знает, как его оптимизировать:
#include <stdio.h> #include <memory.h> #include <limits.h> static inline int unsigned_to_signed(unsigned n) { int result; memcpy( &result, &n, sizeof(result)); return result; } int main(int argc, const char * argv[]) { unsigned int x = UINT_MAX - 1; int xx = unsigned_to_signed(x); return xx; }
для меня (Xcode 8.3.2, Apple LLVM 8.1,- O3), который производит:
_main: ## @main Lfunc_begin0: .loc 1 21 0 ## /Users/Someone/main.c:21:0 .cfi_startproc ## BB#0: pushq %rbp Ltmp0: .cfi_def_cfa_offset 16 Ltmp1: .cfi_offset %rbp, -16 movq %rsp, %rbp Ltmp2: .cfi_def_cfa_register %rbp ##DEBUG_VALUE: main:argc <- %EDI ##DEBUG_VALUE: main:argv <- %RSI Ltmp3: ##DEBUG_VALUE: main:x <- 2147483646 ##DEBUG_VALUE: main:xx <- 2147483646 .loc 1 24 5 prologue_end ## /Users/Someone/main.c:24:5 movl $-2, %eax popq %rbp retq Ltmp4: Lfunc_end0: .cfi_endproc
std::numeric_limits<int>::is_modulo
является константой времени компиляции. таким образом, вы можете использовать его для специализации шаблона. проблема решена, по крайней мере, если компилятор играет вместе с подстановкой.#include <limits> #include <stdexcept> #include <string> #ifdef TESTING_SF bool const testing_sf = true; #else bool const testing_sf = false; #endif // C++ "extensions" namespace cppx { using std::runtime_error; using std::string; inline bool hopefully( bool const c ) { return c; } inline bool throw_x( string const& s ) { throw runtime_error( s ); } } // namespace cppx // C++ "portability perversions" namespace cppp { using cppx::hopefully; using cppx::throw_x; using std::numeric_limits; namespace detail { template< bool isTwosComplement > int signed_from( unsigned const n ) { if( n <= unsigned( numeric_limits<int>::max() ) ) { return static_cast<int>( n ); } unsigned const u_max = unsigned( -1 ); unsigned const u_half = u_max/2 + 1; if( n == u_half ) { throw_x( "signed_from: unsupported value (negative max)" ); } int const i_quarter = static_cast<int>( u_half/2 ); int const int_n1 = static_cast<int>( n - u_half ); int const int_n2 = int_n1 - i_quarter; int const int_n3 = int_n2 - i_quarter; hopefully( n == static_cast<unsigned>( int_n3 ) ) || throw_x( "signed_from: range error" ); return int_n3; } template<> inline int signed_from<true>( unsigned const n ) { return static_cast<int>( n ); } } // namespace detail inline int signed_from( unsigned const n ) { bool const is_modulo = numeric_limits< int >::is_modulo; return detail::signed_from< is_modulo && !testing_sf >( n ); } } // namespace cppp #include <iostream> using namespace std; int main() { int const x = cppp::signed_from( -42u ); wcout << x << endl; }
EDIT: исправлен код, чтобы избежать возможной ловушки на немодульных машинах int (известно, что существует только одна, а именно архаически настроенные версии Unisys Clearpath). Для простоты это делается не поддерживая значение -2n-1 где n числоint
биты значения, на такой машине (т. е. на Clearpath). на практике это значение также не будет поддерживаться машиной (т. е. с представлением знака и величины или дополнением 1).
Я думаю, что тип int составляет не менее двух байтов, поэтому INT_MIN и INT_MAX могут меняться на разных платформах.
это совершенно стандартно-совместимый и будет компилироваться в no-op на MSVC / gcc.
int unsigned_to_signed(unsigned int n) { union UltimateCast { unsigned int In; int Out; } cast; cast.In = n; return cast.Out; }
для вызывающего кода, например:
volatile unsigned int i = 32167; int main() { return unsigned_to_signed( i ); }
мы будем иметь этот вывод сборки (g++ - O3-S):
__Z18unsigned_to_signedj: movl 4(%esp), %eax ret _main: pushl %ebp movl %esp, %ebp andl $-16, %esp call ___main movl _i, %eax leave ret .globl _i .data .align 4 _i: .long 32167
и объявлении
unsigned_to_signed()
какinline
выходы:_main: pushl %ebp movl %esp, %ebp andl $-16, %esp call ___main movl _i, %eax leave ret .globl _i .data .align 4 _i: .long 32167
что довольно аккуратный код.