Эффективное приведение без знака к знаку, избегающее поведения, определяемого реализацией


я хочу определить функцию, которая принимает 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 77

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 могут меняться на разных платформах.

основные типы

≤climits≥ header

это совершенно стандартно-совместимый и будет компилироваться в 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

что довольно аккуратный код.