Macros em C++: beleza perigosa ou mal necessário?
\n\nTodo mundo que já programou em C++ já se deparou com macros. Por um lado, é uma ferramenta poderosa para metaprogramação que pode reduzir a quantidade de código repetitivo. Por outro, as macros são conhecidas por sua natureza traiçoeira. Basta pular um ponto e vírgula ou colocar os parênteses no lugar errado, e o código se transforma em um monstro imprevisível.
\n\nMas existe uma categoria especial de macros em C++ que parece francamente estranha. Você já viu um código assim?
\n\n#define SAFE_DELETE(ptr) do { delete (ptr); (ptr) = nullptr; } while(0)\n\nParece lógico: deletar o ponteiro e defini-lo como nulo. Mas por que esse do-while(0) sem sentido? Um loop que é garantido de executar exatamente uma vez? Parece redundância beirando o absurdo. Ou então este outro exemplo com if-else:
#define LOG_ERROR(msg) if (g_logging) std::cerr << (msg)\n\nParece simples. Mas é exatamente aqui que estão as armadilhas nas quais milhares de desenvolvedores já tropeçaram. Esta pergunta no Stack Overflow acumulou dezenas de milhares de visualizações, e hoje vamos entender por que essas construções não são um bug, mas uma feature, e quais erros de macros C++ elas previnem.
\n\nPor que macros não são funções? O principal problema
\n\nAntes de mergulharmos nos detalhes do do-while(0), vamos lembrar a diferença entre uma macro e uma função comum. Em C++, uma macro é uma substituição textual. O pré-processador simplesmente copia o corpo da macro para o local onde ela é chamada. Sem verificação de tipos, sem escopos — é uma substituição pura de strings.
É por isso que surgem os clássicos erros em macros C++. Vejamos um exemplo simples:
\n\n#define SQUARE(x) x * x\n\nint main() {\n int a = 5;\n int result = SQUARE(a + 1); // Esperamos 36, obtemos 11\n std::cout << result; // Exibirá 11\n return 0;\n}\n\nO que aconteceu? A macro se expandiu para a + 1 * a + 1. Devido à precedência de operadores, a multiplicação foi executada antes da adição. Primeiro erro — falta de parênteses. Vamos corrigir:
#define SQUARE(x) ((x) * (x))\n\nAgora funciona. Mas isso é só o começo. Os problemas reais começam quando a macro contém múltiplas instruções ou construções de controle.
\n\nIdioma do-while(0): escudo contra catástrofes sintáticas
\n\nVoltemos ao nosso SAFE_DELETE. Por que não escrever a macro de forma mais simples, sem esse loop estranho?
#define SAFE_DELETE(ptr) delete (ptr); (ptr) = nullptr;\n\nParece lógico. Mas imagine usá-la neste contexto:
\n\nif (ptr != nullptr)\n SAFE_DELETE(ptr);\nelse\n std::cout << "Pointer is null";\n\nApós a substituição, o pré-processador transformará isso em:
\n\nif (ptr != nullptr)\n delete (ptr);\n (ptr) = nullptr;\nelse\n std::cout << "Pointer is null";\n\nE aí está o bug! O else agora não pertence ao if, porque uma segunda instrução apareceu entre eles. O compilador emitirá um erro, mas a ideia é clara: a macro "quebrou" a construção de controle. É exatamente para esses casos que o do-while(0) é necessário. Ele transforma várias instruções em uma única composta, preservando a lógica:
if (ptr != nullptr)\n do { delete (ptr); (ptr) = nullptr; } while(0);\nelse\n std::cout << "Pointer is null";\n\nAgora tudo funciona corretamente. O idioma de macros do-while(0) é uma técnica padrão que garante que a macro se comporte como uma única instrução.
\n\nPor que exatamente do-while(0) e não apenas um bloco { }?
\n\nBoa pergunta! Poderíamos simplesmente usar chaves:
\n\n#define SAFE_DELETE(ptr) { delete (ptr); (ptr) = nullptr; }\n\nVamos tentar usar