Comparações assinadas / não assinadas

87

Estou tentando entender por que o código a seguir não emite um aviso no local indicado.

//from limits.h
#define UINT_MAX 0xffffffff /* maximum unsigned int value */
#define INT_MAX  2147483647 /* maximum (signed) int value */
            /* = 0x7fffffff */

int a = INT_MAX;
//_int64 a = INT_MAX; // makes all warnings go away
unsigned int b = UINT_MAX;
bool c = false;

if(a < b) // warning C4018: '<' : signed/unsigned mismatch
    c = true;
if(a > b) // warning C4018: '<' : signed/unsigned mismatch
    c = true;
if(a <= b) // warning C4018: '<' : signed/unsigned mismatch
    c = true;
if(a >= b) // warning C4018: '<' : signed/unsigned mismatch
    c = true;
if(a == b) // no warning <--- warning expected here
    c = true;
if(((unsigned int)a) == b) // no warning (as expected)
    c = true;
if(a == ((int)b)) // no warning (as expected)
    c = true;

Achei que tivesse a ver com promoção em segundo plano, mas os dois últimos parecem dizer o contrário.

Na minha opinião, a primeira ==comparação é tanto uma incompatibilidade assinado / não assinado quanto as outras?

Peter
fonte
3
O gcc 4.4.2 imprime um aviso quando invocado com '
-Wall
Isso é especulação, mas talvez esteja otimizando todas as comparações, pois sabe a resposta em tempo de compilação.
Conjunto nulo
2
Ah! ré. Comentário de bobah: Ativei todos os avisos e o aviso ausente agora aparece. Sou de opinião que deveria ter aparecido na mesma configuração de nível de aviso que as outras comparações.
Peter,
1
@bobah: Eu realmente odeio que o gcc 4.4.2 imprima esse aviso (sem nenhuma maneira de dizer a ele para imprimir apenas por desigualdade), já que todas as formas de silenciar esse aviso tornam as coisas piores . A promoção padrão converte confiavelmente -1 ou ~ 0 no valor mais alto possível de qualquer tipo sem sinal, mas se você silenciar o aviso lançando-o você mesmo, terá que saber o tipo exato . Portanto, se você alterar o tipo (estendê-lo, digamos, para unsigned long long), suas comparações com bare -1ainda funcionarão (mas fornecem um aviso), enquanto suas comparações com -1uou (unsigned)-1falharão miseravelmente.
Jan Hudec
Não sei por que você precisa de um aviso e por que os compiladores simplesmente não conseguem fazer funcionar. -1 é negativo, portanto, menor que qualquer número sem sinal. Simples.
CashCow

Respostas:

96

Ao comparar assinado com não assinado, o compilador converte o valor assinado em não assinado. Para igualdade, isso não importa -1 == (unsigned) -1. Para outras comparações é importante, por exemplo, o seguinte é verdadeiro: -1 > 2U.

EDIT: Referências:

5/9: (Expressões)

Muitos operadores binários que esperam operandos do tipo aritmético ou enumeração causam conversões e produzem tipos de resultado de maneira semelhante. O objetivo é produzir um tipo comum, que também é o tipo do resultado. Esse padrão é chamado de conversões aritméticas usuais, que são definidas da seguinte maneira:

  • Se um dos operandos for do tipo long double, o outro deve ser convertido para long double.

  • Caso contrário, se um dos operandos for duplo, o outro deve ser convertido para duplo.

  • Caso contrário, se um dos operandos for float, o outro deve ser convertido para float.

  • Caso contrário, as promoções integrais (4.5) devem ser realizadas em ambos os operandos.54)

  • Então, se um dos operandos for sem sinal longo, o outro deve ser convertido para sem sinal longo.

  • Caso contrário, se um operando for um int longo e o outro int não assinado, então se um int longo puder representar todos os valores de um int não assinado, o int não assinado deverá ser convertido em um int longo; caso contrário, ambos os operandos serão convertidos em unsigned long int.

  • Caso contrário, se um dos operandos for longo, o outro deve ser convertido para longo.

  • Caso contrário, se um dos operandos não tiver sinal, o outro deve ser convertido para sem sinal.

4.7 / 2: (conversões integrais)

Se o tipo de destino não tiver sinal, o valor resultante será o menor inteiro sem sinal congruente com o inteiro de origem (módulo 2 n onde n é o número de bits usados ​​para representar o tipo sem sinal). [Nota: Em uma representação de complemento de dois, esta conversão é conceitual e não há mudança no padrão de bits (se não houver truncamento). ]

EDIT2: Níveis de alerta MSVC

O que é alertado sobre os diferentes níveis de aviso do MSVC são, obviamente, escolhas feitas pelos desenvolvedores. A meu ver, suas escolhas em relação à igualdade assinado / não assinado vs comparações maiores / menores fazem sentido, isso é totalmente subjetivo, é claro:

-1 == -1significa o mesmo que -1 == (unsigned) -1- acho que é um resultado intuitivo.

-1 < 2 não significa o mesmo que -1 < (unsigned) 2- Isso é menos intuitivo à primeira vista e a IMO merece um aviso "mais cedo".

Erik
fonte
Como você pode converter assinados em não assinados? Qual é a versão não assinada do valor assinado -1? (sinal -1 = 1111, enquanto não sinalizado 15 = 1111, bit a bit eles podem ser iguais, mas eles não são logicamente iguais). Eu entendo que se você forçar essa conversão ela funcionará, mas por que o compilador faria isso? É ilógico. Além disso, como comentei acima, quando aumentei os avisos, o aviso faltando == apareceu, o que parece confirmar o que eu digo?
Peter,
1
Como diz 4.7 / 2, assinado para não assinado significa nenhuma mudança no padrão de bits para o complemento de dois. Quanto ao motivo do compilador fazer isso, é exigido pelo padrão C ++. Eu acredito que o raciocínio por trás dos avisos de VS em diferentes níveis é a chance de uma expressão ser não intencional - e eu concordaria com eles que a comparação de igualdade de sinal / sinal é "menos provável" ser um problema do que as comparações de desigualdade. Isso é subjetivo, é claro - essas são escolhas feitas pelos desenvolvedores do compilador VC.
Erik
Ok, acho que quase entendi. A forma como eu li isso é que o compilador está (conceitualmente) fazendo: 'if (((unsigned _int64) 0x7fffffff) == ((unsigned _int64) 0xffffffff))', porque _int64 é o menor tipo que pode representar 0x7fffffff e 0xffffffff em termos não assinados?
Peter
2
Na verdade, comparar com (unsigned)-1ou -1umuitas vezes é pior do que comparar com -1. Isso porque (unsigned __int64)-1 == -1, mas (unsigned __int64)-1 != (unsigned)-1. Portanto, se o compilador der um aviso, você tenta silenciá-lo convertendo em não assinado ou usando -1ue se o valor realmente for de 64 bits ou você alterá-lo para um mais tarde, você quebrará seu código! E lembre-se de que size_tnão tem sinal, 64 bits em plataformas de 64 bits apenas e usar -1 para valor inválido é muito comum com ele.
Jan Hudec
1
Talvez os cpmpilers não devessem fazer isso então. Se comparar com sinal e sem sinal, verifique se o valor com sinal é negativo. Nesse caso, é garantido que seja menor do que o não assinado, independentemente.
CashCow
33

Por que avisos assinados / não assinados são importantes e os programadores devem prestar atenção a eles, é demonstrado pelo exemplo a seguir.

Adivinhe a saída deste código?

#include <iostream>

int main() {
        int i = -1;
        unsigned int j = 1;
        if ( i < j ) 
            std::cout << " i is less than j";
        else
            std::cout << " i is greater than j";

        return 0;
}

Resultado:

i is greater than j

Surpreso? Demonstração online: http://www.ideone.com/5iCxY

Resumindo: em comparação, se um operando for unsigned, o outro operando será convertido implicitamente em unsigned se seu tipo for assinado!

Nawaz
fonte
2
Ele tem razão! É idiota, mas ele está certo. Este é um problema importante que eu nunca encontrei antes. Por que ele não converte o não assinado em um valor com sinal (maior) ?! Se você fizer "if (i <((int) j))", funcionará conforme o esperado. Embora "if (i <((_int64) j))" faça mais sentido (assumindo, o que você não pode fazer, que _int64 tem o dobro do tamanho de int).
Peter
6
@Peter "Por que não converte o não amigo em um valor assinado (maior)?" A resposta é simples: pode não haver um valor maior com sinal. Em uma máquina de 32 bits, nos dias em que não demorou muito, tanto int quanto long eram de 32 bits e não havia nada maior. Ao comparar assinados e não assinados, os primeiros compiladores C ++ converteram ambos em assinados. Por que não me lembro de quais razões, o comitê de padrões C mudou isso. Sua melhor solução é evitar não assinados, tanto quanto possível.
James Kanze
5
@JamesKanze: Suspeito que também tenha a ver com o fato de que o resultado do estouro com sinal é comportamento indefinido, enquanto o resultado do estouro sem sinal não é e, portanto, a conversão de valor com sinal negativo para sem sinal é definida enquanto a conversão de valor sem sinal grande para sinal negativo valor não é .
Jan Hudec
2
@James O compilador sempre poderia gerar um assembly que implementaria a semântica mais intuitiva dessa comparação sem lançar para algum tipo maior. Neste exemplo específico, seria suficiente primeiro verificar se i<0. Então ié menor do que jcom certeza. Se inão for menor que zero, ìpode ser convertido com segurança para sem sinal para comparação j. Claro, as comparações entre com sinal e sem sinal seriam mais lentas, mas seu resultado seria mais correto em algum sentido.
Sven,
@ Mesmo eu concordo. O padrão poderia exigir que as comparações funcionassem para todos os valores reais, em vez de converter para um dos dois tipos. Isso funcionaria apenas para comparações, no entanto; Suspeito que o comitê não queria regras diferentes para comparações e outras operações (e não queria atacar o problema de especificar comparações quando o tipo realmente sendo comparado não existia).
James Kanze,
4

O operador == faz apenas uma comparação bit a bit (por divisão simples para ver se é 0).

Quanto menor / maior do que as comparações, depende muito mais do sinal do número.

Exemplo de 4 bits:

1111 = 15? ou -1?

então se você tem 1111 <0001 ... é ambíguo ...

mas se você tem 1111 == 1111 ... É a mesma coisa, embora você não quisesse que fosse.

Yochai Timmer
fonte
Eu entendo isso, mas não responde à minha pergunta. Como você indicou, 1111! = 1111 se os sinais não corresponderem. O compilador sabe que há uma incompatibilidade entre os tipos, então por que ele não avisa sobre isso? (Meu ponto é que meu código pode conter muitas incompatibilidades das quais não estou sendo avisado.)
Peter,
É a forma como foi projetado. O teste de igualdade verifica a similaridade. E é semelhante. Eu concordo com você que não deveria ser assim. Você poderia fazer uma macro ou algo que sobrecarregue x == y para ser! ((X <y) || (x> y))
Yochai Timmer
1

Em um sistema que representa os valores usando 2-complemento (a maioria dos processadores modernos), eles são iguais mesmo em sua forma binária. Pode ser por isso que o compilador não reclama sobre a == b .

E para mim é estranho o compilador não avisá-lo sobre a == ((int) b) . Acho que deve dar a você um aviso de truncamento de número inteiro ou algo assim.

Hossein
fonte
1
A filosofia do C / C ++ é: o compilador confia que o desenvolvedor sabe o que está fazendo ao converter explicitamente entre os tipos. Portanto, nenhum aviso (pelo menos por padrão - acredito que existam compiladores que geram avisos para isso se o nível de aviso for definido mais alto que o padrão).
Péter Török
0

A linha de código em questão não gera um aviso C4018 porque a Microsoft usou um número de aviso diferente (ou seja, C4389 ) para lidar com esse caso e C4389 não está habilitado por padrão (ou seja, no nível 3).

Dos documentos da Microsoft para C4389:

// C4389.cpp
// compile with: /W4
#pragma warning(default: 4389)

int main()
{
   int a = 9;
   unsigned int b = 10;
   if (a == b)   // C4389
      return 0;
   else
      return 0;
};

As outras respostas explicaram muito bem por que a Microsoft pode ter decidido fazer um caso especial do operador de igualdade, mas acho que essas respostas não são muito úteis sem mencionar C4389 ou como habilitá-lo no Visual Studio .

Devo também mencionar que, se você pretende ativar o C4389, também pode considerar ativar o C4388. Infelizmente, não há documentação oficial para C4388, mas parece aparecer em expressões como as seguintes:

int a = 9;
unsigned int b = 10;
bool equal = (a == b); // C4388
Tim Rae
fonte