Pré-processador C: X macros – Parte 4

funções X macros compilação condicional Diagnóstico
Este post faz parte da série Pré-processador C

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/

Pré-processador C

Pré-processador C: Diagnóstico – Parte 3
Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Notificações
Notificar
8 Comentários
recentes
antigos mais votados
Inline Feedbacks
View all comments
Haroldo Amaral
Haroldo Amaral
08/12/2015 11:48

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

Fernando Deluno Garcia
Fernando Deluno Garcia
Reply to  Haroldo Amaral
10/12/2015 12:40

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

Leonardo José Consoni
Leonardo José Consoni
26/09/2015 14:49

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 »

Leonardo José Consoni
Leonardo José Consoni
Reply to  Leonardo José Consoni
26/09/2015 14:52

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

Fernando Deluno Garcia
Fernando Deluno Garcia
Reply to  Leonardo José Consoni
27/09/2015 20:21

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)?

Leonardo José Consoni
Leonardo José Consoni
Reply to  Fernando Deluno Garcia
27/09/2015 20:52

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

Cesar Junior
Cesar Junior
22/09/2015 08:41

Parabéns pelos artigos! Muito bom.

Fernando Deluno Garcia
Fernando Deluno Garcia
Reply to  Cesar Junior
24/09/2015 09:11

Olá, Cesar.

Fico feliz que tenha gostado da série!

Home » Software » Linguagem C » Pré-processador C: X macros – Parte 4

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste:
Nenhum resultado encontrado.