Elenco não assinado para assinado eficiente, evitando comportamento definido pela implementação


94

Eu quero definir uma função que recebe um unsigned intcomo argumento e retorna um intmódulo congruente UINT_MAX + 1 para o argumento.

Uma primeira tentativa pode ser assim:

int unsigned_to_signed(unsigned n)
{
    return static_cast<int>(n);
}

Mas, como qualquer advogado de linguagem sabe, a conversão de não assinado para assinado para valores maiores que INT_MAX é definida pela implementação.

Desejo implementar isso de forma que (a) dependa apenas do comportamento exigido pela especificação; e (b) compila em um ambiente autônomo em qualquer máquina moderna e otimizando o compilador.

Quanto a máquinas bizarras ... Se não houver nenhum módulo congruente int assinado UINT_MAX + 1 para o int não assinado, digamos que eu queira lançar uma exceção. Se houver mais de um (não tenho certeza se isso é possível), digamos que eu queira o maior.

OK, segunda tentativa:

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
}

Não me importo muito com a eficiência quando não estou em um sistema típico de complemento de dois, pois, em minha humilde opinião, isso é improvável. E se meu código se tornar um gargalo nos sistemas onipresentes de magnitude de sinal de 2050, bem, aposto que alguém pode descobrir isso e otimizá-lo então.

Agora, essa segunda tentativa está bem perto do que eu quero. Embora a conversão para intseja definida pela implementação para algumas entradas, a unsignedconversão de volta para é garantida pelo padrão para preservar o valor módulo UINT_MAX + 1. Portanto, a condicional verifica exatamente o que eu quero e não compila em nenhum sistema que eu possa encontrar.

No entanto ... Ainda estou lançando para intsem primeiro verificar se ele invocará o comportamento definido pela implementação. Em algum sistema hipotético em 2050, poderia fazer sabe-se lá o quê. Então, digamos que eu queira evitar isso.

Pergunta: Qual deve ser a aparência da minha "terceira tentativa"?

Para recapitular, eu quero:

  • Cast de int não assinado para int assinado
  • Preserve o valor mod UINT_MAX + 1
  • Invoque apenas o comportamento obrigatório padrão
  • Compilar em um ambiente autônomo em uma máquina típica de complemento de dois com compilador de otimização

[Atualizar]

Deixe-me dar um exemplo para mostrar por que essa não é uma questão trivial.

Considere uma implementação hipotética de C ++ com as seguintes propriedades:

  • sizeof(int) é igual a 4
  • sizeof(unsigned) é igual a 4
  • INT_MAX é igual a 32767
  • INT_MINé igual a -2 32 + 32768
  • UINT_MAXé igual a 2 32 - 1
  • Em aritmética inté módulo 2 de 32 (para o intervalo INT_MINatravés de INT_MAX)
  • std::numeric_limits<int>::is_modulo é verdade
  • Casting unsigned nto int preserva o valor para 0 <= n <= 32767 e retorna zero caso contrário

Nesta implementação hipotética, há exatamente um intvalor congruente (mod UINT_MAX + 1) para cada unsignedvalor. Então minha pergunta ficaria bem definida.

Eu afirmo que essa implementação hipotética de C ++ está em total conformidade com as especificações C ++ 98, C ++ 03 e C ++ 11. Admito que não memorizei cada palavra de todos eles ... Mas acredito que li as seções relevantes com atenção. Portanto, se quiser que eu aceite sua resposta, você deve (a) citar uma especificação que exclui essa implementação hipotética ou (b) tratá-la corretamente.

Na verdade, uma resposta correta deve lidar com cada implementação hipotética permitida pelo padrão. Isso é o que significa, por definição, "invocar apenas o comportamento determinado por padrão".

A propósito, observe que std::numeric_limits<int>::is_moduloé totalmente inútil aqui por vários motivos. Por um lado, pode ser truemesmo que as conversões não assinadas para assinadas não funcionem para grandes valores não assinados. Por outro lado, pode ser trueaté mesmo nos sistemas de complemento de alguém ou magnitude de sinal, se a aritmética for simplesmente um módulo de todo o intervalo inteiro. E assim por diante. Se sua resposta depender de is_modulo, está errado.

[Atualização 2]

A resposta de hvd me ensinou algo: minha implementação hipotética de C ++ para inteiros não é permitida pelo C. moderno. Os padrões C99 e C11 são muito específicos sobre a representação de inteiros assinados; na verdade, eles apenas permitem complemento de dois, complemento de uns e magnitude de sinal (seção 6.2.6.2 parágrafo (2);).

Mas C ++ não é C. Como descobri, esse fato está no cerne da minha pergunta.

O padrão C ++ 98 original foi baseado no C89 muito mais antigo, que diz (seção 3.1.2.5):

Para cada um dos tipos inteiros com sinal, há um tipo inteiro sem sinal correspondente (mas diferente) (designado com a palavra-chave unsigned) que usa a mesma quantidade de armazenamento (incluindo informações de sinal) e tem os mesmos requisitos de alinhamento. O intervalo de valores não negativos de um tipo inteiro com sinal é um subintervalo do tipo inteiro sem sinal correspondente, e a representação do mesmo valor em cada tipo é a mesma.

C89 não diz nada sobre ter apenas um bit de sinal ou apenas permitir complemento de dois / complemento de uns / magnitude de sinal.

O padrão C ++ 98 adotou esta linguagem quase literalmente (seção 3.9.1 parágrafo (3)):

Para cada um dos tipos de número inteiro com sinal, existe um tipo de número inteiro sem sinal correspondente (mas diferente) : " unsigned char", " unsigned short int", " unsigned int" e " unsigned long int", cada um dos quais ocupa a mesma quantidade de armazenamento e tem os mesmos requisitos de alinhamento (3,9 ) como o tipo inteiro com sinal correspondente; ou seja, cada tipo de inteiro não assinado tem a mesma representação de objeto que seu tipo inteiro não assinado correspondente . A faixa de valores não negativos de um tipo inteiro com sinal é uma subfaixa do tipo inteiro sem sinal correspondente, e a representação do valor de cada tipo com sinal / sem sinal correspondente deve ser a mesma.

O padrão C ++ 03 usa linguagem essencialmente idêntica, assim como o C ++ 11.

Nenhuma especificação C ++ padrão restringe suas representações de inteiros assinados a qualquer especificação C, pelo que posso dizer. E não há nada que obrigue um bit de sinal único ou algo do tipo. Tudo o que diz é que inteiros não negativos com sinal devem ser um subintervalo do não sinal correspondente.

Então, novamente eu afirmo que INT_MAX = 32767 com INT_MIN = -2 32 +32768 é permitido. Se sua resposta presumir o contrário, está incorreta, a menos que você cite um padrão C ++ que prove que estou errado.


@SteveJessop: Na verdade, eu declarei exatamente o que quero nesse caso: "Se não houver nenhum módulo congruente UINT_MAX + 1 com o int não assinado, digamos que eu queira lançar uma exceção." Ou seja, quero o int assinado "certo", desde que exista. Se ele não existir - como pode acontecer no caso de, por exemplo, bits de preenchimento ou representações de complemento de um - quero detectar isso e tratá-lo para essa chamada específica do elenco.
Nemo

desculpe, não sei como perdi isso.
Steve Jessop

A propósito, acho que em sua implementação hipotética complicada intprecisa de pelo menos 33 bits para representá-la. Eu sei que é apenas uma nota de rodapé, então você pode argumentar que não é normativa, mas acho que a nota de rodapé 49 em C ++ 11 se destina a ser verdadeira (já que é uma definição de um termo usado no padrão) e não contradiz qualquer coisa explicitamente declarada no texto normativo. Portanto, todos os valores negativos devem ser representados por um padrão de bits no qual o bit mais alto é definido e, portanto, você não pode agrupá- 2^32 - 32768los em 32 bits. Não que seu argumento dependa de alguma forma do tamanho de int.
Steve Jessop

E com relação às suas edições na resposta do hvd, acho que você interpretou mal a nota 49. Você diz que a magnitude do sinal é proibida, mas não é. Você leu como: "os valores representados por bits sucessivos são aditivos, começam com 1 e (são multiplicados pela potência integral sucessiva de 2, exceto talvez para o bit com a posição mais alta)". Acredito que deva ser lido, "os valores representados por bits sucessivos (são aditivos, começam com 1, e são multiplicados pela potência integral sucessiva de 2), exceto talvez para o bit com a posição mais alta". Ou seja, todas as apostas estão canceladas se o bit alto estiver definido.
Steve Jessop

@SteveJessop: Sua interpretação pode estar correta. Se for assim, isso exclui minha hipótese ... Mas também introduz um número verdadeiramente vasto de possibilidades, tornando essa pergunta extremamente difícil de responder. Isso realmente parece um bug na especificação para mim. (Aparentemente, o comitê C achou que sim e corrigiu totalmente no C99. Eu me pergunto por que o C ++ 11 não adotou sua abordagem?)
Nemo

Respostas:


70

Expandindo a resposta do usuário71404:

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
}

Se x >= INT_MIN(mantenha as regras de promoção em mente, INT_MINé convertido para unsigned), então x - INT_MIN <= INT_MAX, não haverá nenhum estouro.

Se isso não for óbvio, dê uma olhada na afirmação "Se x >= -4u, então x + 4 <= 3." E tenha em mente que INT_MAXserá igual a pelo menos o valor matemático de -INT_MIN - 1.

Nos sistemas mais comuns, onde !(x <= INT_MAX)implica x >= INT_MIN, o otimizador deve ser capaz (e no meu sistema, é capaz) de remover a segunda verificação, determinar que as duas returninstruções podem ser compiladas para o mesmo código e remover a primeira verificação também. Lista de montagem gerada:

__Z1fj:
LFB6:
    .cfi_startproc
    movl    4(%esp), %eax
    ret
    .cfi_endproc

A implementação hipotética em sua pergunta:

  • INT_MAX é igual a 32767
  • INT_MIN é igual a -2 32 + 32768

não é possível, portanto, não requer consideração especial. INT_MINserá igual a -INT_MAXou a -INT_MAX - 1. Isso segue da representação do C de tipos inteiros (6.2.6.2), que exige que os nbits sejam bits de valor, um bit seja um bit de sinal e só permite uma única representação de trap (não incluindo representações que são inválidas por causa de bits de preenchimento), ou seja, aquele que de outra forma representaria zero / negativo -INT_MAX - 1. C ++ não permite nenhuma representação inteira além do que C permite.

Atualização : o compilador da Microsoft aparentemente não percebe issox > 10ex >= 11testa a mesma coisa. Ele só gera o código desejado sex >= INT_MINfor substituído porx > INT_MIN - 1u, que pode detectar como a negação dex <= INT_MAX(nesta plataforma).

[Atualização do questionador (Nemo), elaborando nossa discussão abaixo]

Agora acredito que essa resposta funciona em todos os casos, mas por razões complicadas. É provável que eu concorde com a recompensa por essa solução, mas quero capturar todos os detalhes sangrentos, caso alguém se importe.

Vamos começar com C ++ 11, seção 18.3.3:

A Tabela 31 descreve o cabeçalho <climits>.

...

O conteúdo é o mesmo do cabeçalho da biblioteca C padrão <limits.h>.

Aqui, "Padrão C" significa C99, cuja especificação restringe severamente a representação de inteiros com sinal. Eles são como inteiros sem sinal, mas com um bit dedicado ao "sinal" e zero ou mais bits dedicados ao "preenchimento". Os bits de preenchimento não contribuem para o valor do inteiro e o bit de sinal contribui apenas como complemento de dois, complemento de uns ou magnitude de sinal.

Como o C ++ 11 herda as <climits>macros do C99, INT_MIN é -INT_MAX ou -INT_MAX-1 e o código do hvd tem garantia de funcionamento. (Observe que, devido ao preenchimento, INT_MAX pode ser muito menor do que UINT_MAX / 2 ... Mas, graças à maneira como as conversões com sinal-> sem sinal funcionam, esta resposta funciona bem.)

C ++ 03 / C ++ 98 é mais complicado. Ele usa o mesmo texto para herdar <climits>do "Padrão C", mas agora "Padrão C" significa C89 / C90.

Todos estes - C ++ 98, C ++ 03, C89 / C90 - têm a redação que eu forneço na minha pergunta, mas também incluem isto (C ++ 03 seção 3.9.1 parágrafo 7):

As representações de tipos integrais devem definir valores pelo uso de um sistema de numeração binário puro. (44) [ Exemplo : esta Norma permite o complemento de 2, complemento de 1 e representações de magnitude com sinal para tipos inteiros.]

A nota de rodapé (44) define "sistema de numeração binária puro":

Uma representação posicional para inteiros que usa os dígitos binários 0 e 1, em que os valores representados por bits sucessivos são aditivos, começam com 1 e são multiplicados pela potência integral sucessiva de 2, exceto talvez para o bit com a posição mais alta.

O que é interessante sobre esse texto é que ele se contradiz, pois a definição de "sistema de numeração binária puro" não permite uma representação de sinal / magnitude! Ele permite que o bit alto tenha, digamos, o valor -2 n-1 (complemento de dois) ou - (2 n-1 -1) (complemento de uns). Mas não há valor para o bit alto que resulta em sinal / magnitude.

De qualquer forma, minha "implementação hipotética" não se qualifica como "binário puro" sob esta definição, portanto, está descartada.

No entanto, o fato de que o bit alto é especial significa que podemos imaginá-lo contribuindo com qualquer valor: Um pequeno valor positivo, grande valor positivo, pequeno valor negativo ou grande valor negativo. (Se o bit de sinal pode contribuir - (2 n-1 -1), por que não - (2 n-1 -2)? Etc.)

Então, vamos imaginar uma representação inteira com sinal que atribui um valor estranho ao bit de "sinal".

Um pequeno valor positivo para o bit de sinal resultaria em um intervalo positivo para int(possivelmente tão grande quanto unsigned), e o código de hvd lida com isso muito bem.

Um grande valor positivo para o bit de sinal resultaria em intum máximo maior que unsigned, o que é proibido.

Um grande valor negativo para o bit de sinal resultaria na intrepresentação de uma faixa não contígua de valores, e outras palavras nas especificações excluem isso.

Finalmente, que tal um bit de sinal que contribui com uma pequena quantidade negativa? Poderíamos ter um 1 no "bit de sinal" contribuindo, digamos, -37 para o valor do int? Então INT_MAX seria (digamos) 2 31 -1 e INT_MIN seria -37?

Isso resultaria em alguns números com duas representações ... Mas um complemento dá duas representações para zero, e isso é permitido de acordo com o "Exemplo". Em nenhum lugar a especificação diz que zero é o único inteiro que pode ter duas representações. Portanto, acho que essa nova hipótese é permitida pela especificação.

Na verdade, qualquer valor negativo de -1 até -INT_MAX-1parece ser permitido como um valor para o "bit de sinal", mas nada menor (para que o intervalo não seja contíguo). Em outras palavras, INT_MINpode ser qualquer coisa de -INT_MAX-1-1.

Agora, adivinhe? Para que a segunda conversão no código do hvd evite o comportamento definido pela implementação, precisamos apenas x - (unsigned)INT_MINmenor ou igual a INT_MAX. Acabamos de mostrar que INT_MINé pelo menos-INT_MAX-1 . Obviamente, xé no máximo UINT_MAX. Lançar um número negativo para não assinado é o mesmo que somar UINT_MAX+1. Junte tudo:

x - (unsigned)INT_MIN <= INT_MAX

se e apenas se

UINT_MAX - (INT_MIN + UINT_MAX + 1) <= INT_MAX
-INT_MIN-1 <= INT_MAX
-INT_MIN <= INT_MAX+1
INT_MIN >= -INT_MAX-1

Esse último é o que acabamos de mostrar, então, mesmo neste caso perverso, o código realmente funciona.

Isso esgota todas as possibilidades, encerrando assim este exercício extremamente acadêmico.

Resumindo: há algum comportamento seriamente subespecificado para inteiros assinados em C89 / C90 que foi herdado por C ++ 98 / C ++ 03. Ele é corrigido no C99 e o C ++ 11 indiretamente herda a correção ao incorporar <limits.h>do C99. Mas mesmo o C ++ 11 mantém a formulação contraditória de "representação binária pura" ...


Pergunta atualizada. Estou votando contra esta resposta (por enquanto) para desencorajar os outros ... Vou cancelar a votação mais tarde porque a resposta é interessante. (Correto para C, mas errado para C ++. Eu acho.)
Nemo

@Nemo O padrão C se aplica a C ++ neste caso; no mínimo, os valores em <limits.h>são definidos no padrão C ++ como tendo o mesmo significado que no padrão C, portanto, todos os requisitos de C para INT_MINe INT_MAXsão herdados em C ++. Você está correto que C ++ 03 se refere a C90, e C90 é vago sobre as representações inteiras permitidas, mas a mudança C99 (herdada pelo menos via <limits.h>C ++ 11, esperançosamente também de uma maneira mais direta) para limitá-la a aqueles três eram um que codificava a prática existente: nenhuma outra implementação existia.

Eu concordo que o significado de INT_MINetc. são herdados de C. Mas isso não significa que os valores sejam. (Na verdade, como eles poderiam, uma vez que cada implementação é diferente?) Sua inferência que INT_MINestá dentro de 1 de -INT_MAXdepende de palavras que simplesmente não aparecem em nenhuma especificação C ++. Portanto, embora C ++ herde o significado semântico das macros, a especificação não fornece (ou herda) o texto que apóia sua inferência. Isso parece ser um descuido na especificação C ++ que impede um elenco não assinado para assinado eficiente e totalmente em conformidade.
Nemo

@Nemo Se você (talvez corretamente) afirma que C ++ permite outras representações, então em tal implementação, eu afirmo que INT_MIN não é necessário que seja o valor mínimo representável de tipo int, porque no que diz respeito a C, se o tipo não corresponder aos requisitos de int, o padrão C não pode cobrir essa implementação de forma alguma, e o padrão C ++ não fornece nenhuma definição além de "o que o padrão C diz". Vou verificar se há uma explicação mais direta.

7
Isso é lindo. Não tenho ideia de como eu perdi essa pergunta no momento.
Lightness Races in Orbit

17

Este código depende apenas do comportamento, exigido pela especificação, portanto, o requisito (a) é facilmente satisfeito:

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;
}

Não é tão fácil com o requisito (b). Isso compila em um no-op com gcc 4.6.3 (-Os, -O2, -O3) e com clang 3.0 (-Os, -O, -O2, -O3). Intel 12.1.0 se recusa a otimizar isso. E não tenho informações sobre Visual C.


1
OK, isso é incrível. Eu gostaria de poder dividir o bounty 80:20 ... Suspeito que o raciocínio do compilador é: Se o loop não terminar, resultestoura; estouro de inteiro é indefinido; portanto, o loop termina; portanto, i == nna rescisão; portanto, resulté igual n. Ainda tenho que preferir a resposta do hvd (para o comportamento não patológico em compiladores menos inteligentes), mas isso merece mais votos positivos.
Nemo

1
Não assinados são definidos como módulo. O loop também tem a garantia de terminar porque né algum valor sem sinal e, ieventualmente, deve atingir todos os valores sem sinal.
idupree

7

A resposta original resolveu o problema apenas para unsigned=> int. E se quisermos resolver o problema geral de "algum tipo sem sinal" para seu tipo com sinal correspondente? Além disso, a resposta original foi excelente ao citar seções do padrão e analisar alguns casos extremos, mas realmente não me ajudou a entender por que funcionou, então esta resposta tentará fornecer uma base conceitual forte. Esta resposta tentará ajudar a explicar "por que" e usar recursos modernos do C ++ para tentar simplificar o código.

Resposta C ++ 20

O problema foi simplificado drasticamente com P0907: Inteiros assinados são Complemento de Dois e a redação final P1236 que foi votada no padrão C ++ 20. Agora, a resposta é a mais simples possível:

template<std::unsigned_integral T>
constexpr auto cast_to_signed_integer(T const value) {
    return static_cast<std::make_signed_t<T>>(value);
}

É isso aí. UMAstatic_cast (ou elenco de estilo C) finalmente terá a garantia de fazer o que você precisa para essa questão, e o que muitos programadores pensaram que sempre faria.

Resposta C ++ 17

No C ++ 17, as coisas são muito mais complicadas. Temos que lidar com três possíveis representações inteiras (complemento de dois, complemento de uns e magnitude do sinal). Mesmo no caso em que sabemos que deve ser o complemento de dois, porque verificamos o intervalo de valores possíveis, a conversão de um valor fora do intervalo do inteiro com sinal para esse inteiro com sinal ainda nos dá um resultado definido pela implementação. Temos que usar truques como vimos em outras respostas.

Primeiro, aqui está o código de como resolver o problema genericamente:

template<typename T, typename = std::enable_if_t<std::is_unsigned_v<T>>>
constexpr auto cast_to_signed_integer(T const value) {
    using result = std::make_signed_t<T>;
    using result_limits = std::numeric_limits<result>;
    if constexpr (result_limits::min() + 1 != -result_limits::max()) {
        if (value == static_cast<T>(result_limits::max()) + 1) {
            throw std::runtime_error("Cannot convert the maximum possible unsigned to a signed value on this system");
        }
    }
    if (value <= result_limits::max()) {
        return static_cast<result>(value);
    } else {
        using promoted_unsigned = std::conditional_t<sizeof(T) <= sizeof(unsigned), unsigned, T>;
        using promoted_signed = std::make_signed_t<promoted_unsigned>;
        constexpr auto shift_by_window = [](auto x) {
            // static_cast to avoid conversion warning
            return x - static_cast<decltype(x)>(result_limits::max()) - 1;
        };
        return static_cast<result>(
            shift_by_window( // shift values from common range to negative range
                static_cast<promoted_signed>(
                    shift_by_window( // shift large values into common range
                        static_cast<promoted_unsigned>(value) // cast to avoid promotion to int
                    )
                )
            )
        );
    }
}

Isso tem um pouco mais de conversão do que a resposta aceita, e isso é para garantir que não haja avisos de incompatibilidade assinados / não assinados de seu compilador e para lidar adequadamente com as regras de promoção de inteiros.

Primeiro, temos um caso especial para sistemas que não são complemento de dois (e, portanto, devemos lidar com o valor máximo possível, especialmente porque não tem nada para mapear). Depois disso, chegamos ao algoritmo real.

A segunda condição de nível superior é direta: sabemos que o valor é menor ou igual ao valor máximo, portanto, ele se ajusta ao tipo de resultado. A terceira condição é um pouco mais complicada, mesmo com os comentários, então alguns exemplos provavelmente ajudariam a entender por que cada afirmação é necessária.

Base conceitual: a reta numérica

Primeiro, qual é esse windowconceito? Considere a seguinte reta numérica:

   |   signed   |
<.........................>
          |  unsigned  |

Acontece que, para inteiros de complemento de dois, você pode dividir o subconjunto da reta numérica que pode ser alcançada por qualquer tipo em três categorias de tamanhos iguais:

- => signed only
= => both
+ => unsigned only

<..-------=======+++++++..>

Isso pode ser facilmente comprovado considerando a representação. Um inteiro sem sinal começa em 0e usa todos os bits para aumentar o valor em potências de 2. Um inteiro com sinal é exatamente o mesmo para todos os bits, exceto o bit de sinal, que vale em -(2^position)vez de 2^position. Isso significa que, para todos os n - 1bits, eles representam os mesmos valores. Então, inteiros sem sinal têm mais um bit normal, que dobra o número total de valores (em outras palavras, há tantos valores com aquele bit definido quanto sem ele). A mesma lógica vale para inteiros com sinal, exceto que todos os valores com esse conjunto de bits são negativos.

As outras duas representações inteiras legais, complemento de uns e magnitude de sinal, têm todos os mesmos valores que inteiros de complemento de dois, exceto por um: o valor mais negativo. C ++ define tudo sobre tipos inteiros, exceto para reinterpret_cast(e C ++ 20std::bit_cast ), em termos de intervalo de valores representáveis, não em termos de representação de bits. Isso significa que nossa análise será válida para cada uma dessas três representações, desde que nunca tentemos criar a representação de armadilha. O valor sem sinal que mapearia para esse valor ausente é bastante infeliz: aquele bem no meio dos valores sem sinal. Felizmente, nossa primeira condição verifica (em tempo de compilação) se tal representação existe e, em seguida, trata-a especialmente com uma verificação de tempo de execução.

A primeira condição trata do caso em que estamos na =seção, o que significa que estamos na região de sobreposição onde os valores de uma podem ser representados na outra sem alteração. A região) para que tenhamos um mapeamento exclusivo novamente.shift_by_window função no código move todos os valores para baixo de acordo com o tamanho de cada um desses segmentos (temos que subtrair o valor máximo e depois subtrair 1 para evitar problemas de estouro aritmético). Se estivermos fora dessa região (estamos na +região), precisamos pular para baixo em um tamanho de janela. Isso nos coloca na faixa de sobreposição, o que significa que podemos converter com segurança de sem sinal para sinal, porque não há alteração no valor. No entanto, ainda não terminamos porque mapeamos dois valores sem sinal para cada valor com sinal. Portanto, precisamos descer para a próxima janela (o-

Agora, isso nos dá um mod congruente de resultado UINT_MAX + 1, conforme solicitado na pergunta? UINT_MAX + 1é equivalente a 2^n, onde né o número de bits na representação do valor. O valor que usamos para o tamanho da nossa janela é igual a 2^(n - 1)(o índice final em uma sequência de valores é um a menos que o tamanho). Subtraímos esse valor duas vezes, o que significa que subtraímos 2 * 2^(n - 1)que é igual a 2^n. Adicionar e subtrair xé um modo autônomo no modo aritmético x, então não afetamos o mod de valor original 2^n.

Tratamento adequado de promoções inteiras

Porque esta é uma função genérica e não apenas int e unsigned, também temos que nos preocupar com as regras da promoção integral. Existem dois casos possivelmente interessantes: um em que shorté menor que inte outro em que shorté do mesmo tamanho que int.

Exemplo: shortmenor queint

Se shortfor menor que int(comum em plataformas modernas), também sabemos que unsigned shortpode caber em um int, o que significa que todas as operações nele realmente acontecerão em int, portanto, lançamos explicitamente para o tipo promovido para evitar isso. Nossa declaração final é bastante abstrata e se torna mais fácil de entender se substituirmos por valores reais. Para o nosso primeiro caso interessante, sem perda de generalidade, vamos considerar um 16 bits shorte um 17 bits int(o que ainda é permitido pelas novas regras, e significaria apenas que pelo menos um desses dois tipos inteiros tem alguns bits de preenchimento ):

constexpr auto shift_by_window = [](auto x) {
    return x - static_cast<decltype(x)>(32767) - 1;
};
return static_cast<int16_t>(
    shift_by_window(
        static_cast<int17_t>(
            shift_by_window(
                static_cast<uint17_t>(value)
            )
        )
    )
);

Resolvendo para o maior valor não sinalizado de 16 bits possível

constexpr auto shift_by_window = [](auto x) {
    return x - static_cast<decltype(x)>(32767) - 1;
};
return int16_t(
    shift_by_window(
        int17_t(
            shift_by_window(
                uint17_t(65535)
            )
        )
    )
);

Simplifica para

return int16_t(
    int17_t(
        uint17_t(65535) - uint17_t(32767) - 1
    ) -
    int17_t(32767) -
    1
);

Simplifica para

return int16_t(
    int17_t(uint17_t(32767)) -
    int17_t(32767) -
    1
);

Simplifica para

return int16_t(
    int17_t(32767) -
    int17_t(32767) -
    1
);

Simplifica para

return int16_t(-1);

Colocamos no maior sem assinatura possível e voltamos -1, sucesso!

Exemplo: shortmesmo tamanho queint

Se shortfor do mesmo tamanho que int(incomum em plataformas modernas), a regra de promoção integral é um pouco diferente. Nesse caso, shortpromove para inte unsigned shortpromove para unsigned. Felizmente, convertemos explicitamente cada resultado para o tipo em que queremos fazer o cálculo, portanto, acabamos sem promoções problemáticas. Sem perda de generalidade, vamos considerar um 16 bits shorte um 16 bits int:

constexpr auto shift_by_window = [](auto x) {
    return x - static_cast<decltype(x)>(32767) - 1;
};
return static_cast<int16_t>(
    shift_by_window(
        static_cast<int16_t>(
            shift_by_window(
                static_cast<uint16_t>(value)
            )
        )
    )
);

Resolvendo para o maior valor não sinalizado de 16 bits possível

auto x = int16_t(
    uint16_t(65535) - uint16_t(32767) - 1
);
return int16_t(
    x - int16_t(32767) - 1
);

Simplifica para

return int16_t(
    int16_t(32767) - int16_t(32767) - 1
);

Simplifica para

return int16_t(-1);

Colocamos o maior não assinado possível e voltamos -1, sucesso!

E se eu só se preocupam inte unsignede não se preocupam com os avisos, como a pergunta original?

constexpr int cast_to_signed_integer(unsigned const value) {
    using result_limits = std::numeric_limits<int>;
    if constexpr (result_limits::min() + 1 != -result_limits::max()) {
        if (value == static_cast<unsigned>(result_limits::max()) + 1) {
            throw std::runtime_error("Cannot convert the maximum possible unsigned to a signed value on this system");
        }
    }
    if (value <= result_limits::max()) {
        return static_cast<int>(value);
    } else {
        constexpr int window = result_limits::min();
        return static_cast<int>(value + window) + window;
    }
}

Ver ao vivo

https://godbolt.org/z/74hY81

Aqui, vemos que clang, gcc e icc não geram código para caste cast_to_signed_integer_basicem -O2e -O3, e MSVC não gera código em /O2, portanto, a solução é ótima.


3

Você pode dizer explicitamente ao compilador o que deseja fazer:

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);
  }
}

Compila com gcc 4.7.2for x86_64-linux( g++ -O -S test.cpp) para

_Z18unsigned_to_signedj:
    movl    %edi, %eax
    ret

UINT_MAXé uma expressão de tipo unsigned int, e isso torna todo o seu static_cast<int>(n + INT_MIN) - (UINT_MAX + INT_MIN + 1)tipo. Deve ser possível consertar isso, no entanto, e espero que ainda seja compilado da mesma forma.

2

Se xé nossa contribuição ...

Se x > INT_MAX, queremos encontrar uma constante ktal que 0< x - k*INT_MAX< INT_MAX.

Isso é fácil - unsigned int k = x / INT_MAX;. Então deixaunsigned int x2 = x - k*INT_MAX;

Agora podemos lançar x2com intsegurança. Deixeiint x3 = static_cast<int>(x2);

Agora queremos subtrair algo como UINT_MAX - k * INT_MAX + 1de x3, se k > 0.

Agora, em um sistema de complemento 2s, contanto que x > INT_MAXisso funcione para:

unsigned int k = x / INT_MAX;
x -= k*INT_MAX;
int r = int(x);
r += k*INT_MAX;
r -= UINT_MAX+1;

Observe que UINT_MAX+1 é garantido zero em C ++, a conversão para int foi um noop e subtraímosk*INT_MAX e adicionamos de volta no "mesmo valor". Portanto, um otimizador aceitável deve ser capaz de apagar toda aquela tolice!

Isso deixa o problema de x > INT_MAXou não. Bem, criamos 2 branches, um comx > INT_MAX e outro sem. O que não tem faz um lançamento estreito, que o compilador otimiza para um noop. Aquele com ... faz um noop após o otimizador terminar. O otimizador inteligente realiza as duas ramificações na mesma coisa e descarta a ramificação.

Problemas: se UINT_MAXfor realmente grande em relação a INT_MAX, o acima pode não funcionar. Estou assumindo quek*INT_MAX <= UINT_MAX+1 implicitamente.

Provavelmente poderíamos atacar isso com alguns enums como:

enum { divisor = UINT_MAX/INT_MAX, remainder = UINT_MAX-divisor*INT_MAX };

que funcionam para 2 e 1 em um sistema de complemento 2s, eu acredito (temos garantia de que a matemática funcione? Isso é complicado ...), e fazem a lógica com base neles que otimizam facilmente em sistemas de complemento não-2s ...

Isso também abre o caso de exceção. Isso só é possível se UINT_MAX for muito maior que (INT_MIN-INT_MAX), então você pode colocar seu código de exceção em um bloco if perguntando exatamente essa pergunta de alguma forma, e não vai atrasar você em um sistema tradicional.

Não tenho certeza de como construir essas constantes de tempo de compilação para lidar corretamente com isso.


UINT_MAXnão pode ser pequeno em relação a INT_MAX, porque a especificação garante que todo int com sinal positivo é representável como um int sem sinal. Mas UINT_MAX+1é zero em todos os sistemas; aritmética sem sinal é sempre módulo UINT_MAX+1. Ainda pode haver um núcleo de uma abordagem viável aqui ...
Nemo

@Nemo Apenas acompanhando este tópico, então perdoe minha pergunta potencialmente óbvia: sua afirmação " UINT_MAX+1é zero em todos os sistemas" estabelecida no '03 -spec? Se sim, há uma subseção específica que eu deveria estar procurando? Obrigado.
WhozCraig

@WhozCraig: Seção 3.9.1 parágrafo 4: "Inteiros sem sinal, declarados sem sinal, devem obedecer às leis do módulo aritmético 2 ^ n onde n é o número de bits na representação do valor daquele tamanho particular do inteiro", com uma nota de rodapé dizendo "Isso implica que a aritmética sem sinal não transborda porque um resultado que não pode ser representado pelo tipo inteiro sem sinal resultante é o módulo reduzido do número que é um maior que o maior valor que pode ser representado pelo tipo inteiro sem sinal resultante." Basicamente, unsigned é especificado para funcionar da maneira que você deseja / espera.
Nemo

Obrigado @Nemo. muito apreciado.
WhozCraig

1

std::numeric_limits<int>::is_moduloé uma constante de tempo de compilação. para que você possa usá-lo para especialização de modelo. problema resolvido, pelo menos se o compilador funcionar junto com o inlining.

#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 : Corrigido o código para evitar uma possível armadilha em máquinas não modulares (apenas uma é conhecida, a saber, as versões configuradas arcaicamente do Unisys Clearpath). Para simplificar, isso é feito não suportando o valor -2 n -1, onde n é o número de intbits de valor, nessa máquina (ou seja, no Clearpath). na prática, este valor também não será suportado pela máquina (isto é, com sinal e magnitude ou representação de complemento de 1).


1

Eu acho que o tipo int é de pelo menos dois bytes, então o INT_MIN e INT_MAX podem mudar em plataformas diferentes.

Tipos fundamentais

≤climits≥ cabeçalho


Estou amaldiçoado a usar um compilador para o 6809 que é configurado com "-mint8" por padrão, onde int é de 8 bits :-( (este é o ambiente de desenvolvimento para o Vectrex) long é de 2 bytes, long long é de 4 bytes e Não tenho ideia do que é curto ...
Graham Toal

1

Meu dinheiro é usar o memcpy. Qualquer compilador decente sabe como otimizá-lo:

#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;
}

Para mim (Xcode 8.3.2, Apple LLVM 8.1, -O3), isso produz:

_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

1
Isso não responde à pergunta, já que a representação binária de um sem sinal não é garantida pelo padrão para corresponder à representação assinada.
TLW de
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.