A linguagem C ++ oferece vastas possibilidades para ficar sem macros. Então, vamos tentar usar macros o mínimo possível!
Imediatamente faça uma reserva de que não sou fanático e não desejo abandonar as macros por razões idealistas. Por exemplo, quando se trata de gerar manualmente o mesmo tipo de código, posso reconhecer os benefícios das macros e chegar a um acordo com elas. Por exemplo, eu me relaciono calmamente com macros em programas antigos escritos usando o MFC. Não faz sentido lutar com algo assim:
BEGIN_MESSAGE_MAP(efcDialog, EFCDIALOG_PARENT )
Existem macros e tudo bem. Eles realmente foram criados para simplificar a programação.
Estou falando de outras macros com as quais eles tentam evitar a implementação de uma função completa ou tentam reduzir o tamanho de uma função. Considere vários motivos para evitar essas macros.
Nota Este texto foi escrito como um post convidado para o blog Simplify C ++. Eu decidi publicar a versão russa do artigo aqui. Na verdade, estou escrevendo esta nota para evitar uma pergunta dos leitores desatentos por que o artigo não está marcado como "tradução" :). E aqui, na verdade, uma postagem de convidado em inglês: " Macro Evil in C ++ Code ".Primeiro: o código da macro atrai bugs
Não sei como explicar as razões desse fenômeno do ponto de vista filosófico, mas é. Além disso, os bugs relacionados a macro geralmente são muito difíceis de detectar ao realizar uma revisão de código.
Descrevi repetidamente esses casos em meus artigos. Por exemplo,
substituindo a função
isspace por esta macro:
#define isspace(c) ((c)==' ' || (c) == '\t')
O programador que usou o
isspace acreditava que estava usando uma função real que considera não apenas espaços e tabulações como espaço em branco, mas também LF, CR, etc. O resultado é que uma das condições é sempre verdadeira e o código não funciona conforme o esperado. Este erro do Midnight Commander é descrito
aqui .
Ou como você gosta dessa abreviação para escrever a função
std :: printf ?
#define sprintf std::printf
Eu acho que o leitor acha que foi uma macro muito malsucedida. Foi encontrado, a propósito, no projeto StarEngine. Leia mais sobre isso
aqui .
Alguém poderia argumentar que os programadores são os culpados por esses erros, não por macros. Isso é verdade. Naturalmente, os programadores são sempre os culpados pelos erros :).
É importante que as macros causem erros. Acontece que as macros devem ser usadas com maior precisão ou de maneira nenhuma.
Posso dar exemplos de defeitos associados ao uso de macros por um longo tempo, e essa bela nota se transformará em um documento de várias páginas. É claro que não farei isso, mas mostrarei alguns outros casos para convencer.
A biblioteca ATL
fornece macros como A2W, T2W e assim por diante para converter seqüências de caracteres. No entanto, poucas pessoas sabem que essas macros são muito perigosas para usar dentro de loops. Dentro da macro, a função
alloca é chamada, que alocará memória na pilha repetidas vezes a cada iteração do loop. Um programa pode fingir funcionar corretamente. Assim que o programa começa a processar linhas longas ou o número de iterações no loop aumenta, a pilha pode demorar e terminar no momento mais inesperado. Você pode ler mais sobre isso neste
mini-livro (consulte o capítulo “Não chame a função alloca () dentro dos loops”).
Macros como A2W ocultam o mal. Eles parecem funções, mas, de fato, têm efeitos colaterais difíceis de perceber.
Não consigo passar por tentativas semelhantes de reduzir o código usando macros:
void initialize_sanitizer_builtins (void) { .... #define DEF_SANITIZER_BUILTIN(ENUM, NAME, TYPE, ATTRS) \ decl = add_builtin_function ("__builtin_" NAME, TYPE, ENUM, \ BUILT_IN_NORMAL, NAME, NULL_TREE); \ set_call_expr_flags (decl, ATTRS); \ set_builtin_decl (ENUM, decl, true); #include "sanitizer.def" if ((flag_sanitize & SANITIZE_OBJECT_SIZE) && !builtin_decl_implicit_p (BUILT_IN_OBJECT_SIZE)) DEF_SANITIZER_BUILTIN (BUILT_IN_OBJECT_SIZE, "object_size", BT_FN_SIZE_CONST_PTR_INT, ATTR_PURE_NOTHROW_LEAF_LIST) .... }
Somente a primeira linha da macro se refere à
instrução if . As linhas restantes serão executadas independentemente da condição. Podemos dizer que esse erro é do mundo C, pois foi encontrado por mim usando o diagnóstico
V640 dentro do compilador GCC. O código GCC é escrito principalmente em C, e nesse idioma as macros são difíceis de executar. No entanto, você deve admitir que esse não é o caso. Aqui era bem possível fazer uma função real.
Segundo: a leitura do código se torna mais complicada
Se você se deparar com um projeto cheio de macros consistindo de outras macros, entenderá o que diabos é entender esse projeto. Se você não encontrou, então tome uma palavra, isso é triste. Como um exemplo de código difícil de ler, posso citar o compilador GCC mencionado anteriormente.
Segundo a lenda, a Apple investiu no desenvolvimento do projeto LLVM como uma alternativa ao GCC devido à complexidade do código GCC devido a essas mesmas macros. Onde eu leio sobre isso, não me lembro, então não haverá provas.
Terceiro: escrever macros é difícil
É fácil escrever uma macro incorreta. Eu os encontro em todos os lugares com as conseqüências correspondentes. Mas escrever uma macro boa e confiável geralmente é mais difícil do que escrever uma função semelhante.
Escrever uma boa macro é difícil pelo motivo de que, diferentemente de uma função, ela não pode ser considerada uma entidade independente. É necessário considerar imediatamente a macro no contexto de todas as opções possíveis para seu uso; caso contrário, é muito fácil resolver um problema da forma:
#define MIN(X, Y) (((X) < (Y)) ? (X) : (Y)) m = MIN(ArrayA[i++], ArrayB[j++]);
Obviamente, nesses casos, as soluções alternativas já foram inventadas e a macro pode ser implementada com segurança:
#define MAX(a,b) \ ({ __typeof__ (a) _a = (a); \ __typeof__ (b) _b = (b); \ _a > _b ? _a : _b; })
A única questão é: precisamos de tudo isso em C ++? Não, em C ++ existem modelos e outras maneiras de criar código eficiente. Então, por que continuo encontrando macros semelhantes nos programas C ++?
Quarto: a depuração é complicada
Há uma opinião de que a depuração é para os fracos :). É claro que isso é interessante de discutir, mas do ponto de vista prático, a depuração é útil e ajuda a encontrar erros. As macros complicam esse processo e definitivamente atrasam a busca por erros.
Quinto: falsos positivos dos analisadores estáticos
Muitas macros, devido às especificidades de seus dispositivos, geram vários falsos positivos a partir de analisadores de código estático. Posso dizer com segurança que a maioria dos falsos positivos ao verificar o código C e C ++ está associada a macros.
O problema das macros é que os analisadores simplesmente não conseguem distinguir o código complicado correto do código incorreto. O
artigo sobre a verificação do Chromium descreve uma dessas macros.
O que fazer
Não vamos usar macros em programas C ++, a menos que seja absolutamente necessário!
O C ++ fornece ferramentas avançadas, como funções de modelo, inferência automática de tipo (automático, decltype), funções constexpr.
Quase sempre, em vez de uma macro, você pode escrever uma função comum. Muitas vezes isso não é feito devido à preguiça comum. Essa preguiça é prejudicial, e devemos combatê-la. Um pequeno tempo adicional gasto na criação de uma função completa será recompensado com juros. O código será mais fácil de ler e manter. A probabilidade de fotografar sua própria perna diminuirá e os compiladores e analisadores estáticos produzirão menos falsos positivos.
Alguns podem argumentar que o código com uma função é menos eficiente. Isso também é apenas uma "desculpa".
Agora, os compiladores incorporam perfeitamente o código, mesmo que você não tenha escrito a
palavra-chave inline .
Se estamos falando sobre o cálculo de expressões no estágio de compilação, então aqui as macros não são necessárias nem prejudiciais. Para o mesmo objetivo, é muito melhor e mais seguro usar o
constexpr .
Vou explicar com um exemplo. Aqui está um erro clássico de macro que peguei
emprestado do código do FreeBSD Kernel.
#define ICB2400_VPOPT_WRITE_SIZE 20 #define ICB2400_VPINFO_PORT_OFF(chan) \ (ICB2400_VPINFO_OFF + \ sizeof (isp_icb_2400_vpinfo_t) + \ (chan * ICB2400_VPOPT_WRITE_SIZE))
O argumento
chan é usado em uma macro sem quebra entre parênteses. Como resultado, a expressão
ICB2400_VPOPT_WRITE_SIZE não multiplica a expressão
(chan - 1) , mas apenas uma.
O erro não apareceria se uma função comum fosse gravada em vez de uma macro.
size_t ICB2400_VPINFO_PORT_OFF(size_t chan) { return ICB2400_VPINFO_OFF + sizeof(isp_icb_2400_vpinfo_t) + chan * ICB2400_VPOPT_WRITE_SIZE; }
É muito provável que o compilador C e C ++ moderno execute independentemente o
inlining da função, e o código será tão eficiente quanto no caso de uma macro.
Ao mesmo tempo, o código tornou-se mais legível e livre de erros.
Se for sabido que o valor de entrada é sempre uma constante, você poderá adicionar
constexpr e garantir que todos os cálculos ocorram no estágio de compilação. Imagine que é C ++ e que
chan é sempre uma constante. Então é útil declarar a função
ICB2400_VPINFO_PORT_OFF assim:
constexpr size_t ICB2400_VPINFO_PORT_OFF(size_t chan) { return ICB2400_VPINFO_OFF + sizeof(isp_icb_2400_vpinfo_t) + chan * ICB2400_VPOPT_WRITE_SIZE; }
Lucro!
Espero ter conseguido convencê-lo. Boa sorte e menos macros no código!