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.
#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.
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:
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.
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.
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!
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.
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.
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.
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.
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.
#define MESSAGES sizeof(MessagesLen)
O código final é mostrado abaixo.
#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.
#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.
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.
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.
typedef enum
{
MSG_EXIT,
MSG_HELP,
MSG_CONFIG,
MSG_INIT
}MessageID;
Para definir a tabela de strings uma nova macro é criada.
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.
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.
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.
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.
#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: https://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!