Conversões inteiras (estreitamento, alargamento), comportamento indefinido

Foi muito difícil para mim encontrar informações sobre esse assunto de uma maneira que eu pudesse entender facilmente, por isso estou pedindo uma revisão do que eu descobri. Tudo gira em torno de conversão e conversão.


Nos exemplos, vou me referir a:

(signed/unsigned) int bigger; (signed/unsigned) char smaller; 
  1. Truncar inteiros. (maior-> menor)

    • primeiro truncar bigger no lado MSB para coincidir com tamanho smaller .
    • segundo, converta o resultado truncado para assinado / não assinado, dependendo do tipo menor.

    Se um valor maior for grande demais para caber em um tipo menor, isso resultará em um comportamento indefinido (corrija-me sobre isso). No entanto, minha regra deve estar funcionando em todas as máquinas (corrija-me também) e os resultados devem ser previsíveis.

  2. Widening inteiros (menor-> maior)

    a) signed char -> signed int

    • preceder menor com MSB (1 ou 0) para corresponder ao tamanho maior
    • converter para assinado

    b) signed char -> unsigned int

    • prefixar menor com MSB (1 ou 0) para corresponder ao tamanho maior.
    • converter para não assinado

    c) unsigned char -> signed int

    • prefixar com 0s para corresponder ao tamanho maior
    • converter para assinado

    d) unsigned char -> unsigned int

    • prefixar com 0s para corresponder ao tamanho maior
    • converter para não assinado

Onde estão os comportamentos indefinidos / não especificados que eu não mencionei que poderiam aparecer?

Uma conversão integral nunca produz um comportamento indefinido (pode produzir um comportamento definido pela implementação).

Uma conversão para um tipo que pode representar o valor que está sendo convertido é sempre bem definida: o valor simplesmente permanece inalterado.

Uma conversão para um tipo não assinado é sempre bem definida: o valor é obtido no módulo UINT_MAX + 1 (ou qualquer valor máximo admitido pelo tipo de destino).

Uma conversão para um tipo assinado que não pode representar o valor que está sendo convertido resulta em um valor definido pela implementação ou em um sinal definido pela implementação.

Note que as regras acima são definidas em termos de valores inteiros e não em termos de seqüências de bits.

A partir do documento padrão C (p.50 versão draft 201x eu acredito e não citação exata):

  • Nenhum número inteiro assinado de dois deve ter o mesmo valor

  • A sorting de inteiro assinado deve ser maior que a sorting de qualquer número inteiro com menor precisão.

  • long long int é maior que long int, que é maior que int, que é maior que short int, que é maior que o caractere assinado.

  • assinados e não assinados da mesma precisão têm a mesma sorting (ex: signed int é o mesmo rank que unsigned int)

  • A sorting de qualquer tipo inteiro padrão deve ser maior que a sorting de qualquer tipo inteiro estendido de mesma largura.

  • A sorting de char é igual a unsigned char é igual a char assinado.

(Eu estou deixando de fora bool porque você excluiu da sua pergunta)

  • A sorting de qualquer número inteiro assinado estendido em relação a outro inteiro assinado estendido é definida pela implementação, mas ainda sujeita a outras regras de sorting de conversão de número inteiro.

  • para todos os tipos inteiros T1 T2 e T3, é T1 tem maior sorting que T2 e T2 tem maior sorting que T3, que T1 tem maior sorting que T3.

Um object com um tipo inteiro (diferente de int e com sinal int) cuja sorting inteira é menor que ou igual a rank de int e unsigned int, um campo de tipo de tipo _Bool, int, signed int ou unsigned int; Se um int puder representar todos os valores do tipo original, o valor será convertido em um int. Caso contrário, para um int não assinado. Todos os outros tipos são alterados pela promoção inteira.

Em termos simples:

Qualquer tipo “menor” que int ou unsigned int é promovido para int quando convertido para outro tipo de maior sorting. Este é o trabalho do compilador para garantir que um código C compilado para uma dada máquina (arquitetura) seja compatível com ISO-C a esse respeito. char é implementação definida (sendo assinada ou não assinada). Todos os outros tipos (promoção ou “rebaixamento”) são definidos pela implementação.

O que é definido pela implementação? Isso significa que um determinado compilador se comportará sistematicamente o mesmo em uma determinada máquina. Em outras palavras, todo o comportamento “definido pela implementação” depende AMBOS do compilador E da máquina de destino.

Para tornar o código portátil:

  • sempre promova valores para classificar tipos C padrão mais altos.
  • Nunca “rebaixe” valores para tipos menores.
  • Evite toda implementação “definida pela implementação” em seu código.

Por que essa loucura definida pela implementação existe se arruinar o esforço dos programadores? A programação do sistema basicamente requer esse comportamento definido pela implementação.

Então, mais especificamente para sua pergunta:

  • o truncamento provavelmente não será protegido. Ou exigirá muito mais esforço em manutenção, rastreamento de bugs, etc, do que simplesmente manter o código usando tipos de sorting mais altos.
  • Se sua implementação executar valores maiores que os tipos envolvidos, seu design estará errado (a menos que você esteja envolvido na programação do sistema).
  • Como regra geral, indo de unsigned para assinado preserva valores, mas não o contrário. Portanto, quando um valor não assinado for igual ou superior a um assinado, promova o sinal não assinado como assinado, e não o contrário.
  • Se o uso de tipos inteiros pequenos for crítico para a memory em seu aplicativo, você provavelmente deve revisitar toda a arquitetura do programa.