PHP para Iniciantes: Operadores Bitwise

Agora que falamos sobre operadores matemáticos, vamos nos aprofundar um pouco em lógica de programação? Os operadores desse artigo são, basicamente, formas de manipular os valores binários dos dados, bit a bit, daí o termo bitwise.

Você pode achar que, por se tratar de lógica, isso deve estar relacionado a boolean, mas isso não é totalmente verdade, porém também não é totalmente mentira, rs. O fato é que você não vai usar literalmente um boolean para fazer essas operações. Nós vamos explorar um pouco os bits, mostrando uma visão booleana para esclarecer o que cada operador executa.

Por exemplo, em lógica, você aprende que AND tem a seguinte tabela da verdade:

bool $abool $bAND
falsefalsefalse
falsetruefalse
truefalsefalse
truetruetrue

E essa tabela deixa transparente que a operação AND só dá true quando todos os argumentos envolvidos são true. Enquanto OR:

bool $abool $bOR
falsefalsefalse
falsetruetrue
truefalsetrue
truetruetrue

Transparece que ao menos um dos argumentos precisa ser true.

Obs.: Se você desconhecia essas tabelas da verdade, recomendo pesquisar sobre pois, daqui pra frente será necessário que você entenda sobre lógica de programação para ter mais facilidade em entender o conteúdo transmitido, beleza?

Enfim, conhecendo essas operações, o que acontece com os operadores bitwise é que, ao invés de compararmos dois argumentos simples, nós comparamos o binário entre dois argumentos com um número maior de informações, bit a bit.

Por exemplo, vou deixar aqu uma tabela com números inteiros de 0 a 20 para te mostrar quantos bits cada número representa:

InteiroBinário
00
11
210
311
4100
5101
6110
7111
81000
91001
101010
111011
121100
131101
141110
151111
1610000
1710001
1810010
1910011
2010100

Então, quando usamos um bitwise AND entre os inteiros 15 e 20, o que acontece é:

  • convertemos os inteiros em pacotes de booleanos;
  • 1111 = (true)(true)(true)(true);
  • 10100 = (true)(false)(true)(false)(false);
  • extraímos e comparamos cada booleano, da direita pra esquerda;
  • 0: true and false = false;
  • 1: true and false = false;
  • 2: true and true = true;
  • 3: true and false = false;
  • 4: <null> and true = false;
  • empacotamos o resultado dos bits: (false)(false)(true)(false)(false);
  • removemos os falses à esquerda: (true)(false)(false);
  • convertemos o novo pacote de booleanos em inteiro: 100 = 4;
  • ou seja, 15 bitwise and 20 = 4!

Obs.: mais uma vez, se foi díficil de entender, pesquise sobre lógica de programação. Isso chega a ser uma matéria isolada na faculdade, importantíssima pro bom entendimento dessas operações.

Ok, Kiko, acho que entendi o que rola no bitwise, mas cadê os operadores?

É pra já!

Operador bitwise AND: &

No caso, creio que não preciso explicar pois nosso exemplo acima foi sobre isso. Mas sua escrita é simples: a & b. No caso que mencionei, poderíamos fazer $a = 15 & 20. Se depurar isso, verá que dá 4, como refletimos.

Operador bitwise OR: |

Enquanto & deixa somente os bits que são igualmente verdade nos argumentos, o | vai "somar os 1s". Seguindo nosso exemplo de forma mais resumida, 15 | 20:

  • 15 = 1111;
  • 20 = 10100;
  • 15 | 20 = 11111 (31).

Operador bitwise XOR: ^

Xor é similar a Or, exceto que ele é exclusivo (daí o X na frente). Ou exclusivo é uma operação que considera somente uma das verdades, jamais as duas ao mesmo tempo. Por tanto, em 15 ^ 20 temos:

  • 15 = 1111;
  • 20 = 10100;
  • 15 ^ 20 = 11011 (27), apenas o 1 que tínhamos em comum virou 0.

Operador bitwise NOT: ~

Se o NOT booleano (!) converte true pra false e false pra true, o NOT bit a bit deve negar cada bit, o que significa que, diferente dos operadores anteriores, este só afeta um argumento.

Outro detalhe é que a negação não afeta somente os "bits visíveis" do dado, mas sim todo o comprimento. No caso, se você está rodando o PHP num sistema 64 bits, seu inteiro vai ter 64 bits. Então, ao negar qualquer inteiro, você estará negando 64 bits, não somente a parte com 1s visíveis.

O que é isso que você chama de bit visível, Kiko?

É o que a gente consegue ver antes do primeiro zero à esquerda. Uma boa forma de "enxergar" isso é usar a função decbin do PHP, que exibe o binário de um número decimal. Recomendo usar isso para validar os exemplos abaixo.

Qual a consequência de negar todo o comprimento do inteiro, Kiko?

O interpretador usa o complemento inteiro dos números para detectar seus valores negativos. Por exemplo, se 1 positivo é 1, o -1 é o preenchimento de todos bits restantes com 1. Portanto, negar um inteiro positivo é certo de que irá gerar um inteiro negativo. Vamos ao exemplo?

  • 15 = 0000000000000000000000000000000000000000000000000000000000001111;
  • ~15 = 1111111111111111111111111111111111111111111111111111111111110000 (-16);
  • 20 = 0000000000000000000000000000000000000000000000000000000000010100;
  • ~20 = 1111111111111111111111111111111111111111111111111111111111101011 (-21).

Parece bizarro mas tá tudo certo.

Operador bitwise shift left: <<

Esse é legal pois dá para exemplificar como um fluxo de dados. Por exemplo, se você tem os bits 1010 (10) e quer movê-los um bit para esquerda, fazer << 1 transformará 1010 em 10100 (20). Percebeu que multiplicou por 2? Cada deslocamento é uma multiplicação por 2... Enfim, esse operador é justamente sobre quantas posições você gostaria de mover para a esquerda.

Kiko, o que acontece se o bit passar do limite de comprimento?

Ele se perde, simples assim. Se o limite do inteiro for 4 bits, por exemplo, 10 << 1 resultaria em 0100 (4), pois o primeiro bit iria pro saco.

Kiko, você não vai fazer o exemplo 15 << 20? :hehe:

Não, não tô a fim de sofrer hoje, hahahaha. Mover bits 20 vezes é completamente desnecessário para explicar isso.

Apenas gostaria de deixar uma grande observação: o shift left preserva o bit de positividade (positivo/negativo), então não move exatamente os 64 bits do nossos exemplos anteriores, apenas 63 (ou 31 se o seu sistema for 32 bits).

Operador bitwise shift right: >>

Proporcionalmente igual ao operador anterior, exceto que, ao invés de mover para a esquerda, move para direita. É preciso ressaltar que é muito mais fácil perder dados nesse caso do que o anterior, pois os primeiros bits simplesmente morrem.

Ah, se deslocar para a esquerda significa multiplicar por 2, deslocar para a direita seria dividir, confere? Então vamos testar:

  • 20 = 10100;
  • 20 >> 1 = 1010 (10).

E se movêssemos 2 vezes?

  • 20 >> 2 = 101 (5).

E 3?

  • 20 >> 3 = 10 (2). Aqui vemos um bit se perder.

Kiko, isso não seria uma forma otimizada de fazer divisão por 2, comparando com operadores aritméticos?

Parece mesmo, não é? Mas o interpretador do PHP usa aritmética por baixo dessas operações, o que dá no mesmo. Logo, escreva o que deixa o código mais óbvio, beleza?

E o que acontece se a gente usar esses operadores em texto?

Nem todos vão ter uma tratativa diferente. Especificamente os operadores &, |, ^ e ~ conseguem usar o binário da tabela ASCII e responder a partir dessa tabela. Logo, é possível fazer um AND entre letras para retornar as binariamente semelhantes, embora eu desconheça uma aplicação real disso.

O restante dos operadores irá usar o binário do texto e responder um inteiro.

Ok, Kiko, mas onde raios usaríamos esses operadores?

Lemme show it.

Bitmask

Também conhecido como bit flags, bitmask é uma forma de armazenar um conjunto de possibilidades binárias em uma única variável. Nós conseguimos encontrar isso facilmente em configurações de preferências, isto é, checagens de checkbox que representam as preferências do usuário, por exemplo:

  • [ ] modo escuro (true/false);
  • [ ] alto contraste (true/false);
  • [ ] verificado (true/false);
  • etc.

Quando temos tantas opções, você pode armazenar cada uma delas em variáveis independentes, separadamente, ou em uma única variável usando bitmask. Como diz o nome, você vai criar uma máscara que diz o que representa cada bit:

  • bit 0: modo escuro (1/0);
  • bit 1: alto contraste (1/0);
  • bit 2: verificado (1/0).

Assim, toda a informação fica junta e você consegue separar. O exemplo prático seria:

<?php

class Preferencias
{
    public const MODO_ESCURO = 0b1; // 1
    public const ALTO_CONTRASTE = 0b10; // 2
    public const VERIFICADO = 0b100; // 4
}

$preferenciaArmazenada = 3; // (011), modo escuro + alto contraste

if ($preferenciaArmazenada & Preferencias::MODO_ESCURO) {
    echo "Tem modo escuro" . PHP_EOL;
}

if ($preferenciaArmazenada & Preferencias::ALTO_CONTRASTE) {
    echo "Tem alto contraste" . PHP_EOL;
}

if ($preferenciaArmazenada & Preferencias::VERIFICADO) {
    echo "Tem selo de verificação" . PHP_EOL;
}

Pegue esse exemplo e experimente alterar os valores de $preferenciaArmazenada para:

  • 0: nenhuma preferência;
  • 1: somente modo escuro;
  • 2: somente alto contraste;
  • 3: modo escuro + alto contraste;
  • 4: verificado;
  • 5: modo escuro + verificado;
  • 6: alto contraste + verificado;
  • 7: modo escuro + alto contraste + verificado.

Se você continuar adicionando números, ele vai continuar detectando se tem ou não um dos primeiros 3 bits, a questão é que o teste é desnecessário nessa ocasião.

Com essa máscara de bits, você pode fazer operações ainda mais complexas. Por exemplo, se você quer exibir uma informação somente para quem ativou modo escuro OU alto contraste:

<?php

// cole a classe aqui, hehe

$pref = 3; // vai funcionar para 1, 2, 3, 5, 6 ou 7
$escuroOuAltoContraste = Preferencias::MODO_ESCURO | Preferencias::ALTO_CONTRASTE;

if ($pref & $escuroOuAltoContraste) {
    echo "Olha o brindeeee";
}

Para ou exclusivo, é só trocar o | por ^. Assim, não aceitará quando tem os dois.

Enfim, uma vez levantei uma discussão sobre isso com os devs lá da Picpay, pois tem umas bibliotecas bem interessantes que criaram uma boa legibilidade de código com bitmask (o que é raro), mas armazenar esse aglomerado de binários pode dificultar as análises de banco de dados. Depurar um erro em produção com um dado assim não ajuda, então a conclusão da discussão foi: é melhor manter os dados separados e enxergar com mais clareza o que está sendo parametrizado.

Mas está aí a possibilidade, caso tenha gostado. Você pode encontrar bitmask em projetos de hardware, pois os códigos lá já são bem doidos mesmo. O bitmask nesses casos ajuda bastante a lidar com instruções lógicas com muitos bits de uma só vez.

Por fim... Curtiu? Comenta e compartilha! No próximo artigo falaremos sobre operadores de comparação, que não se resumem a == e ===. São conceitos bem mais simples de entender do que o abordado no artigo de hoje. Tá preparado(a)? Só vamo.

Inté!