The Kass Boy

  • Tech
  • Blog
  • #Sexta
  • Links

Lista de dados em C/C++ com X macros

Lista de dados em C/C++ com X macros


18 de setembro de 2023

Recentemente enquanto assistia a um vídeo do Tsoding(em inglês) fui apresentado a uma técnica extremamente útil e relativamente simples para geração código a partir de uma lista de dados. A técnica chamada de “X macro” utiliza o pré-processador do C e C++ para criar uma lista de macros a ser definida no momento da utilização. Por exemplo, suponha-se que seja necessária uma lista fixa de arquivos que precisam ser utilizados pela aplicação. Inicialmente cria-se uma macro que define uma lista de utilizações de uma outra macro X que ainda não foi definida:

#define LISTA_ARQUIVOS                                        \
X("C:\\\\Users\\juninho-pimpao\\Documents\\arquivo1.txt") \
X("C:\\\\Users\\juninho-pimpao\\Documents\\arquivo2.txt") \
X("C:\\\\Users\\juninho-pimpao\\Documents\\arquivo3.txt")

Esta macro não pode ser utilizada enquanto a macro X não for definida. A definição fica a cargo da utilização em um contexto específico. O nome X é só uma convenção, pode ser qualquer coisa desde que seja o mesmo nome para todos os valores definidos.

O exemplo seguinte imprime os nomes dos arquivos no console:

#define X(nome) printf("- " nome "\n");
LISTA_ARQUIVOS
#undef X

Na primeira linha é definida uma macro X que imprime no console uma linha com valor passado precedido de "- ". A segunda linha expande a macro original substituindo cada linha pelo que foi definido na macro X. Por último, a macro X é “desdefinida” para que possa ser utilizada novamente em outro lugar. O código acima gera a seguinte linha:

printf("- " "C:\\\\Users\\juninho-pimpao\\Documents\\arquivo1.txt" "\n");
printf("- " "C:\\\\Users\\juninho-pimpao\\Documents\\arquivo2.txt" "\n");
printf("- " "C:\\\\Users\\juninho-pimpao\\Documents\\arquivo3.txt" "\n");

O código todo é gerado na mesma linha, mas o ponto e vírgula garante que as expressões sejam interpretadas corretamente. Segue o exemplo completo:

#include <stdio.h>

#define LISTA_ARQUIVOS \
X("C:\\\\Users\\juninho-pimpao\\Documents\\arquivo1.txt") \
X("C:\\\\Users\\juninho-pimpao\\Documents\\arquivo2.txt") \
X("C:\\\\Users\\juninho-pimpao\\Documents\\arquivo3.txt")

int main() {
printf("Arquivos a serem carregados:\n");

#define X(nome) printf("- " nome "\n");
LISTA_ARQUIVOS
#undef X

return 0;
}

que imprime isso no console:

Arquivos a serem carregados:
- C:\\Users\juninho-pimpao\Documents\arquivo1.txt
- C:\\Users\juninho-pimpao\Documents\arquivo2.txt
- C:\\Users\juninho-pimpao\Documents\arquivo3.txt

O que pode ser feito fica a cargo da imaginação do programador e dos limites do pré-processador. O código a seguir complementa o anterior imprimindo também o conteúdo dos arquivos:

#include <stdio.h>
#include <stdlib.h>

#define LISTA_ARQUIVOS \
X("C:\\\\Users\\juninho-pimpao\\Documents\\arquivo1.txt") \
X("C:\\\\Users\\juninho-pimpao\\Documents\\arquivo2.txt") \
X("C:\\\\Users\\juninho-pimpao\\Documents\\arquivo3.txt")

int main() {
printf("Arquivos a serem carregados:\n");

#define X(nome) printf("- " nome "\n");
LISTA_ARQUIVOS
#undef X

printf("\nConteúdo dos arquivos\n");

#define X(nome) \
{ \
printf("***************************\n"); \
printf(nome "\n"); \
FILE* arquivo = fopen(nome, "r"); \
if (!arquivo) { \
printf("\nArquivo não encontrado!\n"); \
} else { \
char *conteudo = NULL; \
size_t tamanho = 0; \
while (getline(&conteudo, &tamanho, arquivo) != -1) { \
printf("%s", conteudo); \
} \
} \
}

LISTA_ARQUIVOS
#undef X

return 0;
}

Esta técnica é bastante útil para definir recursos (assets) em um jogo:

#include <stdlib.h>

#define TEXTURAS \
X(1, TEXTURA_1, "imagens\\textura1.png") \
X(2, TEXTURA_2, "imagens\\textura2.png") \
X(3, TEXTURA_3, "imagens\\textura3.png") \
X(4, TEXTURA_4, "imagens\\textura4.png")

#define X(id, nome, caminho) const unsigned int nome##_ID = id;
TEXTURAS
#undef X

struct Texturas {
#define X(id, nome, caminho) void * nome##_ptr;
TEXTURAS
#undef X
};

extern void * carrega_textura(const char * nome_arquivo);
extern void destroi_textura(void * textura);

int main() {

#define X(id, nome, caminho) void * nome##_ptr = carrega_textura(caminho);
TEXTURAS
#undef X

struct Texturas texturas;
#define X(id, nome, caminho) texturas.nome##_ptr = nome##_ptr;
TEXTURAS
#undef X

#define X(id, nome, caminho) \
destroi_textura(nome##_ptr); \
texturas.nome##_ptr = NULL;

TEXTURAS
#undef X

return 0;
}

que vai gerar o seguinte código (que foi formatado para facilitar a leitura):

#include <stdlib.h>

const unsigned int TEXTURA_1_ID = 1;
const unsigned int TEXTURA_2_ID = 2;
const unsigned int TEXTURA_3_ID = 3;
const unsigned int TEXTURA_4_ID = 4;

struct Texturas {
void * TEXTURA_1_ptr;
void * TEXTURA_2_ptr;
void * TEXTURA_3_ptr;
void * TEXTURA_4_ptr;
};

extern void * carrega_textura(const char * nome_arquivo);
extern void destroi_textura(void * textura);

int main() {

void * TEXTURA_1_ptr = carrega_textura("imagens\\textura1.png");
void * TEXTURA_2_ptr = carrega_textura("imagens\\textura2.png");
void * TEXTURA_3_ptr = carrega_textura("imagens\\textura3.png");
void * TEXTURA_4_ptr = carrega_textura("imagens\\textura4.png");

struct Texturas texturas;

texturas.TEXTURA_1_ptr = TEXTURA_1_ptr;
texturas.TEXTURA_2_ptr = TEXTURA_2_ptr;
texturas.TEXTURA_3_ptr = TEXTURA_3_ptr;
texturas.TEXTURA_4_ptr = TEXTURA_4_ptr;

destroi_textura(TEXTURA_1_ptr);
texturas.TEXTURA_1_ptr = NULL;

destroi_textura(TEXTURA_2_ptr);
texturas.TEXTURA_2_ptr = NULL;

destroi_textura(TEXTURA_3_ptr);
texturas.TEXTURA_3_ptr = NULL;

destroi_textura(TEXTURA_4_ptr);
texturas.TEXTURA_4_ptr = NULL;

return 0;
}

Algumas observações:

  • O código acima não vai compilar pois as funções carrega_textura e destroi_textura não foram definidas.
  • O pré-processador também vai substituir as chamadas as utilizações da macro NULL e #include <stdlib.h>, porém estas alterações foram omitidas para melhoras a legibilidade.

No exemplo a quantidade código gerado é semelhante que à utilizada na geração, mas é preciso lembrar que em uma aplicação real a quantidade de arquivos a ser carregada provavelmente seria muito maior. Além disso, a quantidade de código duplicado é reduzida, reduzindo também a possibilidade de erros de digitação e de “copia e cola”.

Last.fm Letterbox