A arma secreta do depurador: o que programadores C++ ignoram
\n\nVocê já escreveu algo como i = i++ + ++i; e obteve resultados diferentes em compiladores diferentes? Ou já pegou um bug estranho que só aparecia no servidor, mas não no seu notebook? Conheça: undefined behavior (comportamento indefinido) — um dos inimigos mais traiçoeiros do desenvolvedor C++. E no centro desse caos estão os pontos de sequência (sequence points).
Neste artigo, vamos entender o que são sequence points, como eles se relacionam com a ordem de avaliação em C++ e — o mais importante — como não pisar nessas armadilhas. O material é baseado em uma pergunta clássica do Stack Overflow, que recebeu milhares de visualizações. Vamos nessa!
\n\nO que são pontos de sequência (sequence points)?
\n\nImagine que você é o maestro de uma orquestra. Cada músico (instrução do processador) deve entrar em um momento estritamente definido. Um ponto de sequência é um momento em que todos os "efeitos colaterais" anteriores (alterações em variáveis, chamadas de função) já foram garantidamente concluídos, e os próximos ainda não começaram.
\n\nNo padrão C++ 98/03, usava-se o termo sequence point. No C++11, ele foi substituído pelas relações mais rigorosas "sequenciado antes / sequenciado depois", mas a essência permaneceu: entre dois pontos de sequência, o compilador tem total liberdade para reordenar as operações, desde que isso não altere o resultado (de acordo com a regra "as-if").
\n\nSimplificando: se você tenta modificar a mesma variável duas vezes entre dois pontos de sequência — você obtém undefined behavior. O compilador pode fazer qualquer coisa: desde um resultado correto até formatar seu disco rígido (brincadeira, mas quase verdade).
\n\nExemplos clássicos de pontos de sequência
\n- \n
- Ponto e vírgula
;— fim de uma expressão \n - Vírgula
,como operador (mas não em argumentos de função!) \n - Fim de cada expressão em
for(após inicialização, teste e passo) \n - Operadores lógicos
&&e||\n - Operador ternário
?:\n - Chamada de função (antes do corpo da função começar a executar) \n
Como os pontos de sequência geram comportamento indefinido?
\n\nAqui está um exemplo clássico que todo desenvolvedor C++ deveria conhecer:
\n\nint i = 0;\ni = i++ + ++i; // UB! Comportamento indefinido\n\nPor que isso é UB? Porque entre dois pontos de sequência (neste caso, entre o início e o fim da expressão) a variável i é modificada duas vezes:
- \n
- Incremento pós-fixado
i++\n - Incremento pré-fixado
++i\n - Atribuição
i = ...\n
O compilador não sabe em qual ordem executar essas operações. Resultado? Um compilador retorna 2, outro retorna 3, um terceiro retorna 0. E no modo de otimização, pode simplesmente descartar esse código.
\n\nOutro caso complicado: argumentos de função
\n\nint foo(int a, int b) { return a + b; }\n\nint x = 5;\nint result = foo(++x, ++x); // UB!\n\nAqui, a vírgula entre os argumentos não é um operador vírgula, mas um separador. Não há ponto de sequência entre a avaliação dos argumentos ++x! Ambos os incrementos são executados "em paralelo" (do ponto de vista do padrão), e novamente temos undefined behavior.
Exemplos práticos: como o UB se parece em código real
\n\nExemplo 1: Bug não óbvio com indexação
\n\nint arr[5] = {0, 10, 20, 30, 40};\nint idx = 1;\narr[idx] = idx++; // UB!\n\nÀ primeira vista, parece simples: queremos escrever 1 em arr[1] e depois incrementar idx. Mas o padrão não garante a ordem de avaliação do índice idx e do lado direito idx++. Resultado? Pode escrever em arr[1] ou em arr[2].