- Pré-processador C – Parte 1
- Pré-processador C: Compilação condicional – Parte 2
- Pré-processador C: Diagnóstico – Parte 3
- Pré-processador C: X macros – Parte 4
Olá, caro leitor! No último artigo da série Pré-processador C vamos analisar algumas formas de realizar a geração automática de código a partir de uma técnica conhecida como X macros. Para mais informações sobre o pré-processador veja os outros artigos da série, listados no final do artigo. Vamos lá!
Expansão de macros – X macros
X macros é o nome dado para uma técnica de programação que faz uso do pré-processador para construir um mecanismo de geração automática de código.
Para entender essa técnica é necessário ter conhecimento sobre a estrutura básica do pré-processador C e principalmente do mecanismo de definição de macros. Antes de apresentar a ideia geral da técnica, vou utilizar como exemplo um código que apresenta uma tabela de mensagens.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
#include <stdio.h> #include <stdlib.h> /*quantidade de mensagens definidas na aplicação*/ #define MESSAGES 4 /*enumeração para identificar as mensagens da aplicação*/ typedef enum { MSG_EXIT, MSG_HELP, MSG_CONFIG, MSG_INIT }MessageID; /*tabela com as mensagens*/ static char const * const msgTable[MESSAGES] = { "Exit:.....", "Help:.....", "Config:....", "Init:...." }; static void ShowMessage(MessageID id); int main() { ShowMessage(MSG_CONFIG); ShowMessage(MSG_EXIT); ShowMessage(MSG_HELP); /*.....*/ return 0; } /*Função para exibir uma determinada mensagem*/ static void ShowMessage(MessageID id) { int index = (int)id; if(index >= 0 && index < MESSAGES) { printf("%s\r\n", msgTable[index]); } } |
Do código é importante observar que a enumeração e a declaração das mensagens estão relacionadas. Uma pequena alteração na ordem da enumeração ou da declaração das mensagens pode alterar de forma significativa a execução do programa. Para os casos onde temos a definição de muitas informações que estão relacionadas, o recurso de geração automática de código pode ser muito útil.
A ideia da técnica X macros é agregar todos os dados relacionados em uma macro para depois utilizar os recursos de expansão de macros do pré-processador para gerar código automaticamente. Esse procedimento pode ser realizado de duas formas. A primeira é mais simples e serve como base para entender a segunda abordagem.
Primeira abordagem de X macros
De início vamos considerar que em algum ponto do programa exista uma macro com identificador INIT_MESSAGE, que recebe como parâmetro todas as informações que necessitamos. Em um arquivo chamado Messages.h essa macro é utilizada para definir todas as informações que precisamos.
No arquivo Messages.h temos o conteúdo mostrado abaixo.
1 2 3 4 |
INIT_MESSAGE(MSG_EXIT, "Exit:.....") INIT_MESSAGE(MSG_HELP, "Help:.....") INIT_MESSAGE(MSG_CONFIG, "Config:....") INIT_MESSAGE(MSG_INIT, "Init:....") |
Não vamos utilizar aqui o conceito de Header Guards, pois a ideia é que toda vez que o arquivo Messages.h for incluído o seu conteúdo seja copiado, isto é, as chamadas de macro INIT_MESSAGE serão copiadas para o local onde a diretiva #include “Messages.h” foi utilizada.
Voltando ao código de exemplo, podemos definir o enum da seguinte forma:
1 2 3 4 5 6 |
typedef enum { #define INIT_MESSAGE(ID, MESSAGE) ID, #include "Messages.h" #undef INIT_MESSAGE }MessageID; |
No trecho de código mostrado acima ocorre a definição da macro INIT_MESSAGE que possui dois parâmetros. Na sequência o conteúdo do arquivo Messages.h é copiado para o local do #include. O resultado disso é mostrado abaixo.
1 2 3 4 5 6 7 |
typedef enum { INIT_MESSAGE(MSG_EXIT, "Exit:.....") INIT_MESSAGE(MSG_HELP, "Help:.....") INIT_MESSAGE(MSG_CONFIG, "Config:....") INIT_MESSAGE(MSG_INIT, "Init:....") } MessageID; |
Vale lembrar que quando a macro for utilizada o seu identificador será substituído pelo seu valor, nesse caso a expansão da macro resultaria no primeiro parâmetro seguido da vírgula.
1 2 3 4 5 6 7 |
typedef enum { MSG_EXIT, MSG_HELP, MSG_CONFIG, MSG_INIT }MessageID; |
Convém observar que a macro é removida logo após a sua utilização. Já para inicializar a tabela de strings o mesmo processo pode ser realizado!
1 2 3 4 5 6 |
static char const * const msgTable[] = { #define INIT_MESSAGE(ID, MESSAGE) MESSAGE, #include "Messages.h" #undef INIT_MESSAGE }; |
Agora a macro definida é expandida utilizando somente o parâmetro MESSAGE.
1 2 3 4 5 6 7 |
static char const * const msgTable[] = { INIT_MESSAGE(MSG_EXIT, "Exit:.....") INIT_MESSAGE(MSG_HELP, "Help:.....") INIT_MESSAGE(MSG_CONFIG, "Config:....") INIT_MESSAGE(MSG_INIT, "Init:....") }; |
E o resultado final será equivalente à tabela de mensagens do código de exemplo.
1 2 3 4 5 6 7 |
static char const * const msgTable[] = { "Exit:.....", "Help:.....", "Config:....", "Init:...." }; |
Convém observar que para adicionar ou remover uma mensagem fica muito mais simples e as informações localizadas em um único local.
Para saber quantas mensagens foram definidas podemos utilizar uma estrutura composta por variáveis de um byte para representar cada mensagem definida.
1 2 3 4 5 6 |
typedef struct { #define INIT_MESSAGE(ID, MESSAGE) uint8_t ID; #include "Messages.h" #undef INIT_MESSAGE }MessagesLen; |
O resultado da expansão das macros é uma estrutura com elementos de um byte com nome definido pelo parâmetro ID.
1 2 3 4 5 6 7 |
typedef struct { uint8_t MSG_EXIT; uint8_t MSG_HELP; uint8_t MSG_CONFIG; uint8_t MSG_INIT; }MessagesLen; |
Já a macro que representa a quantidade de mensagens pode ser definida da seguinte forma.
1 |
#define MESSAGES sizeof(MessagesLen) |
O código final é mostrado abaixo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
#include <stdio.h> #include <stdlib.h> #include <inttypes.h> typedef struct { #define INIT_MESSAGE(ID, MESSAGE) uint8_t ID; #include "Messages.h" #undef INIT_MESSAGE }MessagesLen; #define MESSAGES sizeof(MessagesLen) /*enumeração para identificar as mensagens da aplicação*/ typedef enum { #define INIT_MESSAGE(ID, MESSAGE) ID, #include "Messages.h" #undef INIT_MESSAGE }MessageID; /*tabela com as mensagens*/ static char const * const msgTable[MESSAGES] = { #define INIT_MESSAGE(ID, MESSAGE) MESSAGE, #include "Messages.h" #undef INIT_MESSAGE }; static void ShowMessage(MessageID id); int main() { ShowMessage(MSG_CONFIG); ShowMessage(MSG_EXIT); ShowMessage(MSG_HELP); /*.....*/ return 0; } /*Função para exibir uma determinada mensagem*/ static void ShowMessage(MessageID id) { int index = (int)id; if(index >= 0 && index < MESSAGES) { printf("%s\r\n", msgTable[index]); } } |
Segunda abordagem de X macros
Na segunda abordagem não é necessário utilizar um arquivo com as chamadas de macro. Nesse caso, a chamada de macro será realizada por outra macro.
1 2 3 4 5 |
#define INIT_MESSAGES(INIT_MESSAGE)\ INIT_MESSAGE(MSG_EXIT, "Exit:.....")\ INIT_MESSAGE(MSG_HELP, "Help:.....")\ INIT_MESSAGE(MSG_CONFIG, "Config:....")\ INIT_MESSAGE(MSG_INIT, "Init:....") |
Observe que as chamadas de macro INIT_MESSAGE serão realizadas na expansão da macro INIT_MESSAGES. Outro ponto importante é que INIT_MESSAGE é um parâmetro da macro INIT_MESSAGES.
Agora podemos definir o enum da seguinte forma.
1 2 3 4 5 6 |
typedef enum { #define EXPAND_ENUM(ID, MESSAGE) ID, INIT_MESSAGES(EXPAND_ENUM) #undef EXPAND_ENUM }MessageID; |
O procedimento é parecido com o mostrado na primeira abordagem. Para configurar os elementos da enumeração a macro INIT_MESSAGES é utilizada e o argumento passado é o identificador EXPAND_ENUM. A expansão da macro INIT_MESSAGES resulta no código mostrado abaixo.
1 2 3 4 5 6 7 |
typedef enum { EXPAND_ENUM(MSG_EXIT, "Exit:.....") EXPAND_ENUM(MSG_HELP, "Help:.....") EXPAND_ENUM(MSG_CONFIG, "Config:....") EXPAND_ENUM(MSG_INIT, "Init:....") } MessageID; |
Já a expansão da macro EXPAND_ENUM resulta no código mostrado abaixo.
1 2 3 4 5 6 7 |
typedef enum { MSG_EXIT, MSG_HELP, MSG_CONFIG, MSG_INIT }MessageID; |
Para definir a tabela de strings uma nova macro é criada.
1 2 3 4 5 6 |
static char const * const msgTable[] = { #define EXPAND_STRINGS(ID, MESSAGE) MESSAGE, INIT_MESSAGES(EXPAND_STRINGS) #undef EXPAND_STRINGS }; |
A expansão da macro INIT_MESSAGES resulta no código abaixo.
1 2 3 4 5 6 7 |
static char const * const msgTable[] = { EXPAND_STRINGS(MSG_EXIT, "Exit:.....") EXPAND_STRINGS(MSG_HELP, "Help:.....") EXPAND_STRINGS(MSG_CONFIG, "Config:....") EXPAND_STRINGS(MSG_INIT, "Init:....") }; |
E por fim.
1 2 3 4 5 6 7 |
static char const * const msgTable[] = { "Exit:.....", "Help:.....", "Config:....", "Init:...." }; |
Da mesma forma como mostrado na primeira abordagem, a quantidade de mensagens pode ser obtida a partir do tamanho de uma struct.
1 2 3 4 5 6 7 8 |
typedef struct { #define EXPAND_STRUCT(ID, MESSAGE) uint8_t ID; INIT_MESSAGES(EXPAND_STRUCT) #undef EXPAND_STRUCT }MessagesLen; #define MESSAGES sizeof(MessagesLen) |
O código final demonstrado na segunda abordagem é mostrado logo abaixo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
#include <stdio.h> #include <stdlib.h> #include <inttypes.h> #define INIT_MESSAGES(INIT_MESSAGE)\ INIT_MESSAGE(MSG_EXIT, "Exit:.....")\ INIT_MESSAGE(MSG_HELP, "Help:.....")\ INIT_MESSAGE(MSG_CONFIG, "Config:....")\ INIT_MESSAGE(MSG_INIT, "Init:....") typedef struct { #define EXPAND_STRUCT(ID, MESSAGE) uint8_t ID; INIT_MESSAGES(EXPAND_STRUCT) #undef EXPAND_STRUCT }MessagesLen; #define MESSAGES sizeof(MessagesLen) typedef enum { #define EXPAND_ENUM(ID, MESSAGE) ID, INIT_MESSAGES(EXPAND_ENUM) #undef EXPAND_ENUM }MessageID; /*tabela com as mensagens*/ static char const * const msgTable[] = { #define EXPAND_STRINGS(ID, MESSAGE) MESSAGE, INIT_MESSAGES(EXPAND_STRINGS) #undef EXPAND_STRINGS }; static void ShowMessage(MessageID id); int main() { ShowMessage(MSG_CONFIG); ShowMessage(MSG_EXIT); ShowMessage(MSG_HELP); /*.....*/ return 0; } /*Função para exibir uma determinada mensagem*/ static void ShowMessage(MessageID id) { int index = (int)id; if(index >= 0 && index < MESSAGES) { printf("%s\r\n", msgTable[index]); } } |
Conclusão
Nesse artigo foi abordada uma técnica de programação que utiliza o pré-processador C como agente para geração automática de código. Fica demonstrado que a técnica X macros possibilita reduzir o processo de repetição de código o que pode ser uma fonte geradora de erros.
Não se pode deixar de fazer algumas comparações com a codificação direta. O trabalho que é facilitado pelo pré-processador é compensado pela legibilidade do código? O tempo para estruturar o mecanismo de geração de código é menor que o tempo perdido na manutenção do código? Acredito que essas questões podem ser respondidas somente pelo programador ou equipe de desenvolvimento frente à aplicação desta técnica em um determinado problema. E você, qual a sua opinião?
Demonstrada essa técnica, chega ao fim a série de artigos sobre o Pré-processador C! Para aprimorar a técnica pesquise mais sobre Metaprogramação utilizando o pré-processador C.
Referências
Fonte da imagem destacada: http://listamaze.com/top-10-programming-languages-for-job-security/
Fernando, é possível criar nomes alternativos para funções, por exemplo para manter compatibilidade com versões antigas? Por exemplo:
// função original
int funcao_original(int a, int b);
// chamada padrão
funcao_original(1, 1);
// chamada alternativa
nome_alternativo(1, 1);
Abraço
Olá, Haroldo.
Não sei se entendi direito sua pergunta. Você quer que a função criada tenha um determinado nome e possa ser chamada por outro?
Uma forma de fazer isso seria criando um #define
#if VERSAO == X
#define NOME_ALTERNATIVO nome_alternativo1
#elif VERSAO == Y
#define NOME_ALTERNATIVO nome_alternativo2
#else
#error Definir o nome alternativo da função …..
#endif
#define MINHA_FUNCAO NOME_ALTERNATIVO
Muito obrigado pelos artigos, em especial esse último, que me forneceu uma ferramenta muito útil para o meu projeto: Com a intenção de simular algo como namespaces em C, estava experimentando usar a seguinte construção: static int MeuNamespace_Funcao1( const char* ); static void MeuNamespace_Funcao2( int, double ); const struct { int (*Funcao1)( const char* ); void (*Funcao2)( int, double ); } MeuNamespace = { .Funcao1 = MeuNamespace_Funcao1, .Funcao2 = MeuNamespace_Funcao2 }; static void FuncaoInterna( int ); Que, a medida que a interface aumentava, me fazia arcar com muita repetição de código e vários erros de compilação por descuido. Com algo… Leia mais »
Minha única frustração foi não ter conseguido colocar todo o processo dentro de uma única macro… por enquanto acho que a única forma seria podendo usar um #define dentro de outro, o que o preprocessador não permite
Olá, Leonardo.
Obrigado pelo retorno!
Para este caso a técnica x macros reduz bastante o trabalho de repetição. Já fiz algo parecido em um sistema dividido em módulos. Ficou bem fácil adicionar/remover funcionalidades.
Quando você fala em utilizar uma macro, significa chamar a macro para realizar todas as configurações necessárias (criar uma determinada macro, usá-la e removê-la)?
Algo como conseguir gerar todo esse código com uma única function-like macro do tipo:
CREATE_INTERFACE( MeuNamespace,
FUNCTION ( int, Funcao1, const char* ),
FUNCTION ( void, Funcao2, int, double ) )
Não encontrei um meio ainda, mas nesse caso é mais um capricho pra condensar o código
Parabéns pelos artigos! Muito bom.
Olá, Cesar.
Fico feliz que tenha gostado da série!