Contexto
Estamos portando código C que foi compilado originalmente usando um compilador C de 8 bits para o microcontrolador PIC. Um idioma comum que foi usado para impedir que variáveis globais não assinadas (por exemplo, contadores de erro) retornem ao zero é o seguinte:
if(~counter) counter++;
O operador bit a bit aqui inverte todos os bits e a instrução só é verdadeira se counter
for menor que o valor máximo. Importante, isso funciona independentemente do tamanho da variável.
Problema
Agora, estamos direcionando um processador ARM de 32 bits usando o GCC. Percebemos que o mesmo código produz resultados diferentes. Até onde sabemos, parece que a operação de complemento bit a bit retorna um valor com um tamanho diferente do que seria de esperar. Para reproduzir isso, compilamos no GCC:
uint8_t i = 0;
int sz;
sz = sizeof(i);
printf("Size of variable: %d\n", sz); // Size of variable: 1
sz = sizeof(~i);
printf("Size of result: %d\n", sz); // Size of result: 4
Na primeira linha de saída, obtemos o que esperávamos: i
é 1 byte. No entanto, o complemento bit a bit de i
na verdade é de quatro bytes, o que causa um problema, porque comparações com isso agora não fornecerão os resultados esperados. Por exemplo, se estiver fazendo (onde i
está devidamente inicializado uint8_t
):
if(~i) i++;
Veremos i
"envolver" de 0xFF para 0x00. Esse comportamento é diferente no GCC em comparação com quando ele costumava funcionar como pretendíamos no compilador anterior e no microcontrolador PIC de 8 bits.
Estamos cientes de que podemos resolver isso lançando da seguinte forma:
if((uint8_t)~i) i++;
Ou por
if(i < 0xFF) i++;
No entanto, em ambas as soluções alternativas, o tamanho da variável deve ser conhecido e é propenso a erros para o desenvolvedor de software. Esses tipos de verificação dos limites superiores ocorrem em toda a base de código. Existem vários tamanhos de variáveis (por exemplo., uint16_t
E unsigned char
etc.) e mudando estes em uma base de código outra forma de trabalho não é algo que nós estamos olhando para frente.
Questão
Nosso entendimento do problema está correto e existem opções disponíveis para resolver isso que não exigem uma nova visita a cada caso em que usamos esse idioma? Nossa suposição está correta, de que uma operação como complemento bit a bit deve retornar um resultado que é do mesmo tamanho que o operando? Parece que isso iria quebrar, dependendo da arquitetura do processador. Sinto que estou tomando pílulas malucas e que C deve ser um pouco mais portátil que isso. Novamente, nossa compreensão disso pode estar errada.
Na superfície, isso pode não parecer um grande problema, mas esse idioma que funcionava anteriormente é usado em centenas de locais e estamos ansiosos para entender isso antes de prosseguir com alterações caras.
Nota: Há uma pergunta duplicada aparentemente semelhante, mas não exata, aqui: A operação bit a bit no char fornece um resultado de 32 bits
Não vi o ponto crucial da questão discutido lá, ou seja, o tamanho do resultado de um complemento bit a bit sendo diferente do que é passado para o operador.
fonte
Respostas:
O que você está vendo é o resultado de promoções com números inteiros . Na maioria dos casos, em que um valor inteiro é usado em uma expressão, se o tipo do valor for menor do que
int
o valor promovidoint
. Isso está documentado na seção 6.3.1.1p2 do padrão C :Portanto, se uma variável tiver o tipo
uint8_t
e o valor 255, o uso de qualquer operador que não seja um elenco ou uma atribuição converterá primeiro em textoint
com o valor 255 antes de executar a operação. É por isso quesizeof(~i)
você fornece 4 em vez de 1.A Seção 6.5.3.3 descreve que promoções com números inteiros se aplicam ao
~
operador:Portanto, assumindo um valor de 32 bits
int
, secounter
tiver o valor de 8 bits,0xff
ele será convertido para o valor de 32 bits0x000000ff
e a aplicação~
será fornecida0xffffff00
.Provavelmente, a maneira mais simples de lidar com isso é sem precisar saber o tipo é verificar se o valor é 0 após o incremento e, em caso afirmativo, diminuí-lo.
A envolvente de números inteiros não assinados funciona em ambas as direções, portanto, decrementar um valor de 0 fornece o maior valor positivo.
fonte
if (!++counter) --counter;
para alguns programadores, pode ser menos estranho do que usar o operador de vírgula.++counter; counter -= !counter;
.increment_unsigned_without_wraparound
ouincrement_with_saturation
. Pessoalmente, eu usaria uma função genérica de três operandosclamp
.em tamanho de (i); você solicita o tamanho da variável i , então 1
em tamanho de (~ i); você solicita o tamanho do tipo da expressão, que é um int , no seu caso 4
Usar
para saber se eu não valorizo 255 (no seu caso com o uint8_t) não é muito legível, basta fazer
e você terá um código portátil e legível
Para gerenciar qualquer tamanho de não assinado:
A expressão é constante, então calculada em tempo de compilação.
#include <limits.h> para CHAR_BIT e #include <stdint.h> para uintmax_t
fonte
!= 255
é inadequada.unsigned
objetos, pois as mudanças na largura total do objeto não são definidas pelo padrão C, mas podem ser corrigidas com(2u << sizeof(i)*CHAR_BIT-1) - 1
.((uintmax_t) 2 << sizeof(i)*CHAR_BIT-1) - 1
.Aqui estão várias opções para implementar "Adicionar 1 ao
x
clamp no valor máximo representável", considerando quex
há algum tipo de número inteiro não assinado:Adicione um se e somente se
x
for menor que o valor máximo representável em seu tipo:Consulte o item a seguir para a definição de
Maximum
. Esse método tem uma boa chance de ser otimizado por um compilador para obter instruções eficientes, como uma comparação, alguma forma de conjunto ou movimentação condicional e um complemento.Compare com o maior valor do tipo:
(Isso calcula 2 N , onde N é o número de bits
x
, deslocando 2 por N -1 bits. Fazemos isso em vez de mudar 1 N bits, porque uma mudança no número de bits de um tipo não é definida por C ACHAR_BIT
macro pode não ser familiar para alguns; é o número de bits em um byte, assimsizeof x * CHAR_BIT
como o número de bits no tipo dex
.)Isso pode ser envolvido em uma macro, conforme desejado, para estética e clareza:
Incremente
x
e corrija se ele chegar a zero, usando umif
:Incremente
x
e corrija se o valor é zero, usando uma expressão:Isso é nominalmente sem ramificação (às vezes benéfico para o desempenho), mas um compilador pode implementá-lo da mesma forma acima, usando uma ramificação, se necessário, mas possivelmente com instruções incondicionais, se a arquitetura de destino tiver instruções adequadas.
Uma opção sem ramificação, usando a macro acima, é:
Se
x
for o máximo de seu tipo, será avaliado comox += 1-1
. Caso contrário, éx += 1-0
. No entanto, a divisão é um pouco lenta em muitas arquiteturas. Um compilador pode otimizar isso para instruções sem divisão, dependendo do compilador e da arquitetura de destino.fonte
-Wshift-op-parentheses
. A boa notícia é que um compilador otimizador não irá gerar uma divisão aqui, então você não precisa se preocupar com a lentidão.sizeof x
não pode ser implementado dentro de uma função C porquex
teria que ser um parâmetro (ou outra expressão) com algum tipo fixo. Não foi possível produzir o tamanho de qualquer tipo de argumento usado pelo chamador. Uma lata de macro.Antes de stdint.h, os tamanhos das variáveis podem variar de compilador para compilador e os tipos de variáveis reais em C ainda são int, long, etc, e ainda são definidos pelo autor do compilador quanto ao seu tamanho. Não existem algumas premissas padrão nem específicas. O (s) autor (es) precisa criar stdint.h para mapear os dois mundos; esse é o objetivo do stdint.h para mapear o uint_this que int, long, short.
Se você está portando código de outro compilador e ele usa char, short, int, long, é necessário passar por cada tipo e fazer a porta você mesmo, não há como contorná-lo. E você acaba com o tamanho certo para a variável, a declaração muda, mas o código, conforme escrito, funciona ....
ou ... forneça a máscara ou a impressão direta diretamente
No final do dia, se você deseja que esse código funcione, é necessário portá-lo para a nova plataforma. Sua escolha sobre como. Sim, você precisa gastar o tempo necessário em cada caso e fazê-lo corretamente, caso contrário, continuará voltando a esse código, que é ainda mais caro.
Se você isolar os tipos de variáveis no código antes de portar e qual o tamanho dos tipos de variáveis, isole as variáveis que fazem isso (deve ser fácil grep) e altere suas declarações usando as definições stdint.h que esperamos que não sejam alteradas no futuro, e você ficaria surpreso, mas os cabeçalhos errados são usados algumas vezes, então faça check-ins para que você possa dormir melhor à noite
E embora esse estilo de codificação funcione (se (~ contador) contador ++;), para a portabilidade desejar agora e no futuro, é melhor usar uma máscara para limitar especificamente o tamanho (e não confiar na declaração), faça isso quando o o código é escrito em primeiro lugar ou apenas termina a porta e você não precisará reportá-lo novamente outro dia. Ou, para tornar o código mais legível, faça o if x <0xFF then ou x! = 0xFF ou algo assim, o compilador pode otimizá-lo no mesmo código que faria para qualquer uma dessas soluções, apenas o torna mais legível e menos arriscado ...
Depende da importância do produto ou de quantas vezes você deseja enviar patches / atualizações ou rolar um caminhão ou caminhar até o laboratório para corrigir a questão de tentar encontrar uma solução rápida ou apenas tocar nas linhas de código afetadas. se é apenas uma centena ou poucas que não é tão grande de uma porta.
fonte
Rascunho Online do C 2011
O problema é que o operando de
~
está sendo promovido paraint
antes da aplicação do operador.Infelizmente, acho que não há uma maneira fácil de resolver isso. Escrita
não ajudará porque as promoções também se aplicam a ele. A única coisa que posso sugerir é criar algumas constantes simbólicas para o valor máximo que você deseja que esse objeto represente e teste com relação a isso:
fonte
-1
não é necessário, pois isso faria com que o contador se estabelecesse em 254 (0xFE). De qualquer forma, essa abordagem, como mencionado na minha pergunta, não é ideal devido a diferentes tamanhos de variáveis na base de código que participam desse idioma.