Quais são as práticas recomendadas para ints não assinados?

43

Uso entradas não assinadas em todos os lugares e não tenho certeza se devo. Pode ser das colunas de identificação da chave primária do banco de dados aos contadores, etc. Se um número nunca deve ser negativo, sempre utilizarei um int não assinado.

No entanto, percebo pelo código de outros que mais ninguém parece fazer isso. Existe algo crucial que eu estou ignorando?

Edit: Desde essa pergunta, eu também notei que em C, retornar valores negativos para erros é comum em vez de gerar exceções, como em C ++.

wting
fonte
26
Apenas atente para for(unsigned int n = 10; n >= 0; n --)(laços infinitamente)
Chris Burt-Brown
3
Em C e C ++, entradas não assinadas definiram com precisão o comportamento de estouro (módulo 2 ^ n). Entradas assinadas não. Os otimizadores exploram cada vez mais esse comportamento indefinido de estouro, levando a resultados surpreendentes em alguns casos.
Steve314
2
Boa pergunta! Eu também já fui tentado a usar as restrições para restringir o alcance, mas descobri que o risco / inconveniente superava qualquer benefício / conveniência. A maioria das bibliotecas, como você disse, aceita ints regulares onde um uint faria. Isso dificulta o trabalho, mas também levanta a questão: vale a pena? Na prática (supondo que você não faça as coisas de maneira tola), você raramente terá um valor de -218, onde é esperado um valor positivo. Que -218 deve ter vindo de algum lugar, certo? e você pode rastrear sua origem. Acontece raramente. Utilize asserções, exceções, contratos de código para ajudá-lo.
@ William Ting: Se se trata apenas de C / C ++, você deve adicionar as tags apropriadas à sua pergunta.
CesarGon
2
@ Chris: Quão significativo é o problema do loop infinito na realidade? Quero dizer, se for lançado, o código obviamente não foi testado. Mesmo quando você precisar de algumas horas para depurá-lo na primeira vez que cometer esse erro, na segunda vez deverá saber o que procurar primeiro quando seu código não parar de repetir.
Secure

Respostas:

28

Existe algo crucial que eu estou ignorando?

Quando os cálculos envolvem tipos assinados e não assinados, além de tamanhos diferentes, as regras para promoção de tipos podem ser complexas e levar a um comportamento inesperado .

Acredito que essa seja a principal razão pela qual o Java omitiu tipos int não assinados.

Michael Borgwardt
fonte
3
Outra solução seria exigir que você expressasse manualmente seus números, conforme apropriado. Isto é o que o Go parece fazer (só brinquei com ele um pouquinho), e eu gosto mais do que a abordagem do Java.
Tikhon Jelvis
2
Essa foi uma boa razão para o Java não incluir um tipo não assinado de 64 bits e talvez um motivo decente para não incluir um tipo não assinado de 32 bits [embora a semântica de adicionar valores de 32 bits assinados e não assinados não seja difícil - essa operação deve simplesmente produzir um resultado assinado de 64 bits]. Tipos não assinados menores do que intnão apresentariam tal dificuldade, no entanto (uma vez que qualquer cálculo promoverá a int); Não tenho nada de bom a dizer sobre a falta de um tipo de byte não assinado.
Supercat 3/14
17

Eu acho que Michael tem um ponto válido, mas na IMO a razão pela qual todo mundo usa int o tempo todo (especialmente em for (int i = 0; i < max, i++) é que nós aprendemos dessa maneira. Quando todos os exemplos de um livro de ' como aprender programação ' são usados intem forloop, muito poucos questionam essa prática.

A outra razão é que inté 25% menor que uint, e somos todos preguiçosos ... ;-)

Treb
fonte
2
Eu concordo com a questão educacional. A maioria das pessoas parece nunca questionar o que lê: se está em um livro, não pode estar errado, certo?
Matthieu M.
1
Provavelmente, também é por isso que todo mundo usa o postfix ++ao incrementar, apesar do fato de que seu comportamento específico raramente é necessário e pode até levar a agitar as cópias sem sentido se o índice do loop for um iterador ou outro tipo não fundamental (ou o compilador for realmente denso) .
Underscore_d
Apenas não faça algo como "for (uint i = 10; i> = 0; --i)". Usar apenas ints para variáveis ​​de loop evita essa possibilidade.
David Thornley
11

A codificação das informações do intervalo em tipos é uma coisa boa. Impõe o uso de números razoáveis ​​no momento da compilação.

Muitas arquiteturas parecem ter instruções especializadas para lidar com int-> floatconversões. A conversão de unsignedpode ser mais lenta (um pouquinho) .

Benjamin Bannier
fonte
8

A mistura de tipos assinados e não assinados pode levar você a um mundo de dor. E você não pode usar todos os tipos não assinados porque encontrará itens que possuem um intervalo válido que inclui números negativos ou precisam de um valor para indicar um erro e -1 é o mais natural. Portanto, o resultado final é que muitos programadores usam todos os tipos de números inteiros assinados.

David Schwartz
fonte
1
Talvez seja uma prática melhor não misturar valores válidos com indicação de erro na mesma variável e usar variáveis ​​separadas para isso. Concedido, a biblioteca padrão C não é um bom exemplo aqui.
Secure
7

Para mim, os tipos são muito sobre comunicação. Usando explicitamente um int não assinado, você me diz que os valores assinados não são válidos. Isso me permite adicionar algumas informações ao ler seu código, além do nome da variável. Idealmente, um tipo não anônimo me diria mais, mas isso me dá mais informações do que se você tivesse usado ints em todos os lugares.

Infelizmente, nem todo mundo está muito consciente sobre o que o código comunica, e essa é provavelmente a razão pela qual você vê ints em todos os lugares, mesmo que os valores sejam pelo menos sem sinal.

daramarak
fonte
4
Mas talvez eu queira restringir meus valores por um mês a 1 a 12 apenas. Eu uso outro tipo para isso? Que tal um mês? Alguns idiomas permitem restringir valores como esse. Outros, como .Net / C #, fornecem contratos de código. Certamente, números inteiros não negativos ocorrem com bastante frequência, mas a maioria dos idiomas que oferecem suporte a esse tipo não oferece suporte a restrições adicionais. Então, deve-se usar uma mistura de dicas e verificação de erros, ou apenas fazer tudo através da verificação de erros? A maioria das bibliotecas não pede uint onde faria sentido usar uma, portanto, usar uma e a conversão podem ser inconvenientes.
Job
@ Job: Eu diria que você deve usar algum tipo de restrição imposta pelos compiladores / intérpretes nos seus meses. Isso pode lhe dar alguns clichês para configurar, mas, no futuro, você terá uma restrição imposta que evita erros e comunica muito mais claramente o que você espera. Prevenir erros e facilitar a comunicação são muito mais importantes do que inconvenientes durante a implementação.
daramarak
1
"Talvez eu queira restringir meus valores de um mês para 1 a 12 apenas" Se você tiver um conjunto finito de valores como meses, deverá usar um tipo de enumeração, não números inteiros brutos.
Josh Caswell
6

Eu uso unsigned intem C ++ para índices de matriz, principalmente, e para qualquer contador que comece a partir de 0. Eu acho bom dizer explicitamente "essa variável não pode ser negativa".

quant_dev
fonte
14
Você provavelmente deve estar usando size_t para isso em c ++
JohnB
2
Eu sei, simplesmente não posso me incomodar.
1_11
3

Você deve se preocupar com isso quando estiver lidando com um número inteiro que possa realmente se aproximar ou exceder os limites de um int assinado. Como o máximo positivo de um número inteiro de 32 bits é 2.147.483.647, você deve usar um int sem sinal se souber que a) nunca será negativo eb) poderá atingir 2.147.483.648. Na maioria dos casos, incluindo chaves e contadores de banco de dados, nunca abordarei esses tipos de números, para não me preocupar em me preocupar se o bit de sinal é usado para um valor numérico ou para indicar o sinal.

Eu diria: use int, a menos que você saiba que precisa de um int não assinado.

Joel Etherton
fonte
2
Ao trabalhar com valores que podem atingir os valores máximos, você deve começar a verificar as operações para estouros de número inteiro, independentemente do sinal. Essas verificações geralmente são mais fáceis para tipos não assinados, porque a maioria das operações tem resultados bem definidos, sem comportamento indefinido e definido pela implementação.
Secure
3

É uma troca entre simplicidade e confiabilidade. Quanto mais erros puderem ser detectados em tempo de compilação, mais confiável será o software. Diferentes pessoas e organizações estão em pontos diferentes ao longo desse espectro.

Se você faz alguma programação de alta confiabilidade no Ada, você ainda usa tipos diferentes para variáveis ​​como distância em pés x distância em metros, e o compilador o sinaliza se você atribuir acidentalmente um ao outro. Isso é perfeito para programar um míssil guiado, mas é um exagero se você estiver validando um formulário da web. Não há necessariamente nada de errado com os dois, desde que atenda aos requisitos.

Karl Bielefeldt
fonte
2

Estou inclinado a concordar com o raciocínio de Joel Etherton, mas chego à conclusão oposta. Do meu ponto de vista, mesmo que você saiba que é improvável que os números se aproximem dos limites de um tipo assinado, se você sabe que números negativos não acontecerão, há muito pouco motivo para usar a variante assinada de um tipo.

Pelo mesmo motivo, em algumas instâncias selecionadas, usei BIGINT(número inteiro de 64 bits) em vez de INTEGER(número inteiro de 32 bits) nas tabelas do SQL Server. A probabilidade de os dados atingirem o limite de 32 bits dentro de um período de tempo razoável é minúscula, mas se isso acontecer, as consequências em algumas situações poderão ser bastante devastadoras. Apenas certifique-se de mapear os tipos entre os idiomas corretamente, ou você vai acabar com uma estranheza interessante muito abaixo da estrada ...

Dito isto, para algumas coisas, como valores de chave primária de banco de dados, assinados ou não assinados, realmente não importa, porque, a menos que você esteja reparando manualmente dados quebrados ou algo nesse sentido, nunca estará lidando com o valor diretamente; é um identificador, nada mais. Nesses casos, a consistência é provavelmente mais importante que a escolha exata da assinatura. Caso contrário, você terá algumas colunas de chave estrangeira assinadas e outras não assinadas, sem um padrão aparente - ou com essa estranheza interessante novamente.

um CVn
fonte
Se você estiver trabalhando com dados extraídos de um sistema SAP, recomendo fortemente o BIGINT para campos de ID (como CustomerNumber, ArticleNumber etc.). Enquanto ninguém usa cordas alfanuméricos como IDs, que é ... suspiro
Treb
1

Eu recomendaria que, fora dos contextos de armazenamento e intercâmbio de dados com restrição de espaço, usássemos tipos assinados. Na maioria dos casos, em que um número inteiro assinado de 32 bits seria muito pequeno, mas um valor não assinado de 32 bits seria suficiente para hoje, não demorará muito para que o valor não assinado de 32 bits também não seja grande o suficiente.

O principal momento em que se deve usar tipos não assinados é quando se está montando vários valores em um valor maior (por exemplo, convertendo quatro bytes em um número de 32 bits) ou decompondo valores maiores em valores menores (por exemplo, armazenando um número de 32 bits em quatro bytes ), ou quando se tem uma quantidade que se espera "rolar" periodicamente e é preciso lidar com isso (pense em um medidor de utilidade residencial; a maioria deles tem dígitos suficientes para garantir que não possam rolar entre as leituras se forem lidos três vezes por ano, mas não o suficiente para garantir que não rolem durante a vida útil do medidor). Tipos não assinados geralmente têm 'estranheza' suficiente para serem usados ​​apenas nos casos em que sua semântica é necessária.

supercat
fonte
1
"Eu recomendaria [...] geralmente usar tipos assinados". Hm, você esqueceu de mencionar as vantagens dos tipos assinados e forneceu apenas uma lista de quando usar tipos não assinados. "estranheza" ? Enquanto a maioria das operações não assinadas possui comportamento e resultados bem definidos, insira um comportamento indefinido e definido pela implementação ao usar tipos assinados (estouro, deslocamento de bits, ...). Você tem uma definição estranha de "estranheza" aqui.
Secure
1
@ Secure: A "estranheza" a que me refiro tem a ver com a semântica dos operadores de comparação, especialmente em operações que envolvem tipos assinados e não assinados. Você está certo de que o comportamento dos tipos assinados é indefinido ao usar valores grandes o suficiente para transbordar, mas o comportamento dos tipos não assinados pode ser surpreendente, mesmo ao lidar com números relativamente pequenos. Por exemplo, (-3) + (1u) é maior que -1. Além disso, algumas relações associativas matemáticas normais que se aplicariam a números não se aplicam a não assinadas. Por exemplo, (ab)> c não implica (ac)> b.
Supercat
1
@ Seguro: Embora seja verdade que nem sempre é possível confiar nesse comportamento associativo com números assinados "grandes", os comportamentos funcionam como esperado ao lidar com números "pequenos" em relação ao domínio de números inteiros assinados. Por outro lado, a não associação mencionada acima é problemática com valores não assinados "2 3 1". Aliás, o fato de comportamentos assinados terem um comportamento indefinido quando usados ​​fora dos limites pode permitir uma geração melhorada de código em algumas plataformas ao usar valores menores que o tamanho da palavra nativa.
Supercat
1
Se esses comentários estivessem em sua resposta em primeiro lugar, em vez de uma recomendação e "xingamentos" sem fornecer nenhum motivo, eu não teria comentado. ;) Embora eu ainda não concorde com "estranheza" aqui, é simplesmente a definição do tipo. Use a ferramenta certa para o trabalho especificado e conheça a ferramenta, é claro. Tipos não assinados são a ferramenta errada quando você precisa de relações +/-. Há uma razão pela qual size_tnão está assinado e ptrdiff_testá assinado.
Secure
1
@ Secure: Se o que se quer é representar uma sequência de bits, os tipos não assinados são ótimos; Eu acho que concordamos lá. E em alguns micros pequenos, os tipos não assinados podem ser mais eficientes para quantidades numéricas. Eles também são úteis nos casos em que deltas representam quantidades numéricas, mas os valores reais não (por exemplo, números de sequência TCP). Por outro lado, sempre que subtraímos valores não assinados, precisamos nos preocupar com maiúsculas e minúsculas, mesmo quando os números são pequenos; essas matemáticas com valores assinados apresentam apenas cantos quando os números são grandes.
Supercat
1

Uso ints não assinados para tornar meu código e sua intenção mais claros. Uma coisa que faço para me proteger contra conversões implícitas inesperadas ao fazer aritmética com os tipos assinado e não assinado é usar um atalho não assinado (normalmente 2 bytes) para minhas variáveis ​​não assinadas. Isso é eficaz por alguns motivos:

  • Quando você faz aritmética com suas variáveis ​​curtas não assinadas e literais (que são do tipo int) ou variáveis ​​do tipo int, isso garante que a variável não assinada seja sempre promovida a um int antes de avaliar a expressão, pois int sempre tem uma classificação mais alta que curta . Isso evita qualquer comportamento inesperado ao fazer aritmética com tipos assinados e não assinados, assumindo que o resultado da expressão se encaixe em um int assinado, é claro.
  • Na maioria das vezes, as variáveis ​​não assinadas que você está usando não excederão o valor máximo de um curto não assinado de 2 bytes (65.535)

O princípio geral é que o tipo de suas variáveis ​​não assinadas deve ter uma classificação inferior ao tipo das variáveis ​​assinadas para garantir a promoção para o tipo assinado. Então você não terá nenhum comportamento inesperado de estouro. Obviamente, você não pode garantir isso o tempo todo, mas (na maioria das vezes) é possível garantir isso.

Por exemplo, recentemente tive alguns para o loop algo parecido com isto:

const unsigned short cuint = 5;
for(unsigned short i=0; i<10; ++i)
{
    if((i-2)%cuint == 0)
    {
       //Do something
    }
}

O literal '2' é do tipo int. Se eu fosse um int sem sinal em vez de um curto sem sinal, na sub-expressão (i-2), 2 seria promovido a um int sem sinal (já que o int sem sinal tem uma prioridade mais alta que o sinal assinado). Se i = 0, a sub-expressão é igual a (0u-2u) = algum valor massivo devido ao estouro. A mesma idéia com i = 1. No entanto, como i é um curto não assinado, ele é promovido para o mesmo tipo que o literal '2', que é assinado int, e tudo funciona bem.

Para maior segurança: no caso raro em que a arquitetura que você está implementando causa 2 int, isso pode fazer com que os dois operandos na expressão aritmética sejam promovidos a int sem sinal no caso em que a variável curta não assinada não se encaixa no int de 2 bytes assinado, este último com um valor máximo de 32.767 <65.535. (Consulte https://stackoverflow.com/questions/17832815/c-implicit-conversion-signed-unsigned para obter mais detalhes). Para se proteger, você pode simplesmente adicionar um static_assert ao seu programa da seguinte maneira:

static_assert(sizeof(int) == 4, "int must be 4 bytes");

e não será compilado em arquiteturas onde int é 2 bytes.

AlmiranteAdama
fonte