Usando números inteiros não assinados em C e C ++

23

Eu tenho uma pergunta muito simples que me confunde por um longo tempo. Como estou lidando com redes e bancos de dados, muitos dados são contadores de 32 e 64 bits (não assinados), IDs de identificação de 32 e 64 bits (também não possuem mapeamento significativo para sinal). Praticamente nunca lido com qualquer palavra real que possa ser expressa como um número negativo.

Eu e meus colegas de trabalho usamos rotineiramente tipos não assinados como uint32_te uint64_tpara esses assuntos e, como acontece com muita frequência, também os usamos para índices de matriz e outros usos inteiros comuns.

Ao mesmo tempo, vários guias de codificação que estou lendo (por exemplo, Google) desencorajam o uso de tipos inteiros não assinados e, tanto quanto sei, nem Java nem Scala possuem tipos inteiros não assinados.

Portanto, não consegui descobrir o que é certo: usar valores assinados em nosso ambiente seria muito inconveniente, ao mesmo tempo em que codificamos guias para insistir em fazer exatamente isso.

zzz777
fonte

Respostas:

31

Existem duas escolas de pensamento sobre isso, e nenhuma delas jamais concordará.

O primeiro argumenta que existem alguns conceitos que são inerentemente não assinados - como índices de matriz. Não faz sentido usar números assinados para aqueles, pois isso pode levar a erros. Ele também pode impor limites desnecessários às coisas - uma matriz que usa índices assinados de 32 bits pode acessar apenas 2 bilhões de entradas, enquanto a mudança para números de 32 bits não assinados permite 4 bilhões de entradas.

O segundo argumenta que, em qualquer programa que use números não assinados, mais cedo ou mais tarde, você terminará fazendo uma aritmética mista com sinal sem sinal. Isso pode gerar resultados estranhos e inesperados: converter um grande valor não assinado em assinado dá um número negativo e, inversamente, converter um número negativo em não assinado dá um grande número positivo. Isso pode ser uma grande fonte de erros.

Simon B
fonte
8
Problemas aritméticos assinados e não assinados são detectados pelo compilador; apenas mantenha sua construção livre de aviso (com um nível de aviso alto o suficiente). Além disso, inté mais curto para digitar :)
rucamzu
7
Confissão: estou na segunda escola de pensamento e, embora compreenda as considerações para tipos não assinados: inté mais do que suficiente para índices de matriz em 99,99% das vezes. Os problemas aritméticos assinados e não assinados são muito mais comuns e, portanto, têm precedência em termos do que evitar. Sim, os compiladores o alertam sobre isso, mas quantos avisos você recebe ao compilar qualquer projeto considerável? Ignorar avisos é perigoso e é uma prática ruim, mas no mundo real ...
Elias Van Ootegem
11
+1 na resposta. Cuidado : Opiniões contundentes à frente : 1: Minha resposta à segunda escola de pensamento é: Aposto que quem obtém resultados inesperados de tipos integrais não assinados em C terá um comportamento indefinido (e não do tipo puramente acadêmico) em seus programas C não triviais que usam tipos integrais assinados . Se você não conhece C o suficiente para pensar que tipos não assinados são os melhores para usar, aconselho a evitar C. 2: Existe exatamente um tipo correto para índices e tamanhos de matriz em C, e é isso size_t, a menos que haja um caso especial boa razão caso contrário.
Mtraceur
5
Você se depara com problemas sem ter uma identidade mista. Apenas calcule int sem sinal menos int sem sinal.
gnasher729
4
Não tendo problemas com você, Simon, apenas com a primeira escola de pensamento que argumenta que "existem alguns conceitos que são inerentemente não assinados - como índices de matriz". especificamente: "Existe exatamente um tipo correto para índices de matriz ... em C", besteira! . Nós, DSPs, usamos índices negativos o tempo todo. particularmente com respostas de impulso par ou simetria ímpar que não são causais. e para matemática LUT. eu sou na segunda escola de pensamento, mas eu acho que é útil ter ambos inteiros assinados e não assinados em C e C ++.
22618 Robert Bristow-Johnson
21

Em primeiro lugar, a diretriz de codificação do Google C ++ não é muito boa a seguir: evita coisas como exceções, impulso, etc., que são os grampos do C ++ moderno. Em segundo lugar, apenas porque uma determinada diretriz funciona para a empresa X não significa que será a opção certa para você. Eu continuaria usando tipos não assinados, pois você precisa deles.

Uma regra intprática decente para C ++ é: prefira, a menos que você tenha um bom motivo para usar outra coisa.

bstamour
fonte
8
Não é isso que eu quero dizer. Os construtores são para estabelecer invariantes e, como não são funções, não podem simplesmente return falsese essa invariante não for estabelecida. Portanto, você pode separar as coisas e usar as funções init para seus objetos, ou pode executar um std::runtime_error, deixar o desenrolar da pilha acontecer e permitir que todos os seus objetos RAII se limpem automaticamente e você, o desenvolvedor, possa lidar com a exceção onde for conveniente. você faz isso.
bstamour
5
Não vejo como o tipo de aplicativo faz a diferença. Sempre que você chama um construtor em um objeto, está estabelecendo uma invariável com os parâmetros. Se essa invariante não puder ser atendida, será necessário sinalizar um erro, caso contrário, seu programa não está em bom estado. Como os construtores não podem retornar um sinalizador, lançar uma exceção é uma opção natural. Forneça um argumento sólido sobre o motivo pelo qual um aplicativo de negócios não se beneficiaria desse estilo de codificação.
precisa saber é o seguinte
8
Duvido muito que metade de todos os programadores de C ++ sejam incapazes de usar exceções corretamente. De qualquer forma, se você acha que seus colegas de trabalho são incapazes de escrever C ++ moderno, fique longe do C ++ moderno.
precisa saber é
6
@ zzz777 Não usa exceções? Os construtores privados que são invólucros por funções públicas de fábrica que capturam as exceções e fazem o que - retornam a nullptr? retornar um objeto "padrão" (o que isso possa significar)? Você não resolveu nada - acabou de esconder o problema debaixo de um tapete e espera que ninguém descubra.
Mael
5
@ zzz777 Se você vai travar a caixa de qualquer maneira, por que se importa se isso acontece de uma exceção ou signal(6)? Se você usar uma exceção, os 50% dos desenvolvedores que souberem lidar com eles poderão escrever um bom código e o restante poderá ser realizado por seus colegas.
usar o seguinte
6

As outras respostas não têm exemplos do mundo real, então adicionarei um. Uma das razões pelas quais eu (pessoalmente) tento evitar tipos não assinados.

Considere usar size_t padrão como um índice de matriz:

for (size_t i = 0; i < n; ++i)
    // do something here;

Ok, perfeitamente normal. Em seguida, considere que decidimos alterar a direção do loop por algum motivo:

for (size_t i = n - 1; i >= 0; --i)
    // do something here;

E agora não funciona. Se intusássemos como iterador, não haveria problema. Eu já vi esse erro duas vezes nos últimos dois anos. Uma vez que aconteceu na produção e foi difícil de depurar.

Outro motivo para mim são avisos irritantes, que fazem você escrever algo assim toda vez :

int n = 123;  // for some reason n is signed
...
for (size_t i = 0; i < size_t(n); ++i)

Essas são coisas menores, mas somadas. Eu sinto que o código é mais limpo se apenas números inteiros assinados são usados ​​em todos os lugares.

Edit: Claro, os exemplos parecem idiotas, mas eu vi pessoas cometendo esse erro. Se existe uma maneira tão fácil de evitá-lo, por que não usá-lo?

Ao compilar o seguinte trecho de código com o VS2015 ou o GCC, não vejo avisos com as configurações de aviso padrão (mesmo com -Wall for GCC). Você precisa solicitar ao -Wextra para receber um aviso sobre isso no GCC. Esse é um dos motivos pelos quais você deve sempre compilar com o Wall e o Wextra (e usar o analisador estático), mas em muitos projetos da vida real as pessoas não fazem isso.

#include <vector>
#include <iostream>


void unsignedTest()
{
    std::vector<int> v{ 1, 2 };

    for (int i = v.size() - 1; i >= 0; --i)
        std::cout << v[i] << std::endl;

    for (size_t i = v.size() - 1; i >= 0; --i)
        std::cout << v[i] << std::endl;
}

int main()
{
    unsignedTest();
    return 0;
}
Aleksei Petrenko
fonte
Você pode entendê-lo ainda mais errado com tipos assinados ... E seu código de exemplo é tão mortal e totalmente errado que qualquer compilador decente avisará se você solicitar avisos.
Deduplicator
1
No passado, recorri a horrores que o for (size_t i = n - 1; i < n; --i)fizeram funcionar corretamente.
Simon B
2
Falando em for-loops com o size_tinverso, há uma diretriz de codificação no estilo defor (size_t revind = 0u; revind < n; ++revind) { size_t ind = n - 1u - revind; func(ind); }
rwong 15/10/16
2
@rwong Omg, isso é feio. Por que não usar apenas int? :)
Aleksei Petrenko,
1
@AlexeyPetrenko - observe que nem os padrões C ou C ++ atuais garantem que intseja grande o suficiente para armazenar todos os valores válidos de size_t. Particularmente, intpode permitir números apenas até 2 ^ 15-1, e geralmente o faz em sistemas que possuem limites de alocação de memória de 2 ^ 16 (ou, em certos casos, até mais altos). longpode ser uma aposta mais segura, embora ainda não esteja garantida que funcione. Apenas size_té garantido que funcione em todas as plataformas e em todos os casos.
Jules
4
for (size_t i = v.size() - 1; i >= 0; --i)
   std::cout << v[i] << std::endl;

O problema aqui é que você escreveu o loop de uma maneira pouco inteligente, levando a um comportamento incorreto. A construção do loop é como os iniciantes ensinam para tipos assinados (o que é correto e correto), mas simplesmente não se encaixa em valores não assinados. Mas isso não pode servir como contra-argumento contra o uso de tipos não assinados, a tarefa aqui é simplesmente acertar seu loop. E isso pode ser facilmente corrigido para funcionar de maneira confiável para tipos não assinados, como:

for (size_t i = v.size(); i-- > 0; )
    std::cout << v[i] << std::endl;

Essa alteração simplesmente reverte a sequência da operação de comparação e decremento e, na minha opinião, é a maneira mais eficaz, imperturbável, limpa e curta de lidar com contadores não assinados em loops invertidos. Você faria a mesma coisa (intuitivamente) ao usar um loop while:

size_t i = v.size();
while (i > 0)
{
    --i;
    std::cout << v[i] << std::endl;
}

Nenhum subfluxo pode ocorrer, o caso de um contêiner vazio é coberto implicitamente, como na variante conhecida do loop de contador assinado, e o corpo do loop pode permanecer inalterado em comparação com um contador assinado ou um loop de avanço. Você só precisa se acostumar com a construção de loop de aparência um tanto estranha. Mas depois de ver isso uma dúzia de vezes, não há mais nada ininteligível.

Eu teria sorte se os cursos para iniciantes mostrassem não apenas o loop correto para tipos assinados, mas também para tipos não assinados. Isso evitaria alguns erros que devem ser atribuídos aos desenvolvedores inconscientes em vez de culpar o tipo não assinado.

HTH

Don Pedro
fonte
1

Inteiros não assinados estão lá por um motivo.

Considere, por exemplo, entregar dados como bytes individuais, por exemplo, em um pacote de rede ou em um buffer de arquivo. Ocasionalmente, você pode encontrar animais como números inteiros de 24 bits. Desloque facilmente os bits de três números inteiros não assinados de 8 bits, não tão fácil com números inteiros assinados de 8 bits.

Ou pense em algoritmos usando tabelas de pesquisa de caracteres. Se um caractere for um número inteiro não assinado de 8 bits, você poderá indexar uma tabela de pesquisa por um valor de caractere. No entanto, o que você faz se a linguagem de programação não suportar números inteiros não assinados? Você teria índices negativos para uma matriz. Bem, acho que você poderia usar algo assim, charval + 128mas isso é simplesmente feio.

De fato, muitos formatos de arquivo usam números inteiros não assinados e, se a linguagem de programação do aplicativo não suportar números inteiros não assinados, isso pode ser um problema.

Em seguida, considere os números de sequência TCP. Se você escrever qualquer código de processamento TCP, definitivamente desejará usar números inteiros não assinados.

Às vezes, a eficiência é tão importante que você realmente precisa desse bit extra de números inteiros não assinados. Considere, por exemplo, dispositivos de IoT que são enviados em milhões. Muitos recursos de programação podem ser justificados para serem gastos em micro-otimizações.

Eu argumentaria que a justificativa para evitar o uso de tipos inteiros não assinados (aritmética de sinais mistos, comparações de sinais mistos) pode ser superada por um compilador com avisos adequados. Esses avisos geralmente não são ativados por padrão, mas veja, por exemplo, -Wextraou separadamente -Wsign-compare(ativado automaticamente em C por -Wextra, embora eu não ache que seja ativado automaticamente em C ++) e -Wsign-conversion.

No entanto, em caso de dúvida, use um tipo assinado. Muitas vezes, é uma escolha que funciona bem. E ative esses avisos do compilador!

juhist
fonte
0

Existem muitos casos em que números inteiros não representam números, mas, por exemplo, uma máscara de bit, um ID etc. Basicamente, casos em que adicionar 1 a um número inteiro não tem nenhum resultado significativo. Nesses casos, use não assinado.

Existem muitos casos em que você faz aritmética com números inteiros. Nesses casos, use números inteiros assinados para evitar mau comportamento em torno de zero. Veja muitos exemplos com loops, nos quais a execução de um loop até zero usa código muito pouco intuitivo ou é interrompida devido ao uso de números não assinados. Existe o argumento "mas os índices nunca são negativos" - com certeza, mas as diferenças de índices, por exemplo, são negativas.

No caso muito raro em que os índices excedem 2 ^ 31, mas não 2 ^ 32, você não usa números inteiros não assinados, usa números inteiros de 64 bits.

Finalmente, uma boa armadilha: em um loop "for (i = 0; i <n; ++ i) a [i] ..." se i não tiver 32 bits e a memória exceder os endereços de 32 bits, o compilador não pode otimizar o acesso a um [i] incrementando um ponteiro, porque em i = 2 ^ 32 - 1 eu envolvo. Mesmo quando n nunca fica tão grande. O uso de números inteiros assinados evita isso.

gnasher729
fonte
-5

Por fim, encontrei uma resposta muito boa aqui: "Secure Programming Cookbook", de J.Viega e M.Messier ( http://shop.oreilly.com/product/9780596003944.do )

Problemas de segurança com números inteiros assinados:

  1. Se a função requer um parâmetro positivo, é fácil esquecer a verificação da faixa inferior.
  2. Padrão de bits não intuitivo das conversões negativas de tamanho inteiro.
  3. Padrão de bits não intuitivo produzido pela operação de deslocamento à direita de um número inteiro negativo.

Há problemas com as conversões <-> não assinadas assinadas, portanto, não é aconselhável usar o mix.

zzz777
fonte
1
Por que é uma boa resposta? O que é receita 3.5? O que diz sobre excesso de número etc?
Baldrickk
Na minha experiência prática, é um livro muito bom, com conselhos valiosos, todos os outros aspectos que tentei, e é bastante firme nessa recomendação. Comparando com os perigos do excesso de números inteiros em matrizes maiores que 4G, parece bastante fraco. Se eu tiver que lidar com matrizes tão grandes, meu programa terá muitos ajustes finos para evitar penalidades de desempenho.
Zzz777
1
não se trata de saber se o livro é bom. Sua resposta não fornece nenhuma justificativa para o uso do destinatário, e nem todos terão uma cópia do livro para procurá-lo. Olhe para os exemplos de como escrever uma boa resposta
Baldrickk
A FYI aprendeu sobre outro motivo do uso de números inteiros não assinados: é possível detectar facilmente o estouro: youtube.com/…
zzz777