Macros Pré-Definidas em C e C++

por Fabio A. Mazzarino

Tanto a linguagem C, como a linguagem C++, contam com um conjunto de macros pré-definidas. Elas estão resolvidas em tempo de pré-processamento, portanto antes mesmo da compilação, e podem ser muito úteis. Vamos conhecê-las e estudar utilidades para o dia-a-dia.

Pré-Compilação

O processo de geração de executáveis em C++ passam basicamente por três passos. No primeiro deles as diretivas de pré-compilação são substituídas. Diretivas como #define, #include, #ifdef, etc, são devidamente resolvidas e substituídas, sejam localmente, sejam no resto do código. Depois das substituições o texto resultante é passado ao compilador e em seguida para o linker.

Macros são diretivas de pré-compilação que são substituídas no código durante o processo de pré-compilação. Portanto, nome de arquivo, número de linha, data e hora, são informações referentes ao tempo de pré-compilação.

__FILE__, __LINE__ e __func__

As macros __FILE__ e __LINE__ são resolvidas como o nome e a linha corrente do arquivo, respectivamente, novamente em tempo de pré-compilação. __func__ é resolvida como o nome da função, porém é padrão somente a partir de C99.

No caso de pré-compiladores externos, como no caso de Oracle Pro*C, o número da linha será referente ao arquivo .c gerado pelo Pro*C e não referente ao arquivo .pc gerado pelo desenvolvedor. Isso vale para qualquer outro pré-compilador.

Vamos a um exemplo prático:

fprintf(stderr, "Falha de alocacao de memoria em %s:%d (%s)\n", __FILE__, __LINE__, __func__);

O código acima irá escreve algo mais ou menos assim em stderr:

Falha de alocacao de memoria em macros.c:5 (readconfig)

A utilização do nome do arquivo, da linha do arquivo e do nome da função é muito útil no processo de depuração de erros, indicando claramente aonde o problema foi encontrado. Para erros fatais e irrecuperáveis, principalmente fora do ambiente produtivo, é uma ótima ferramenta para indicar aonde o erro aconteceu.

Há algum tempo no Lab C++ um exemplo de Trace de Alocação em C ANSI, utilizando __LINE__ e __FILE__. Tem um snippet de código muito útil para quem quer evitar vazamentos de memória.

__DATE__ e __TIME__

As macros __DATE__ e __TIME__ são resolvidas como a data e hora da execução do pré-processamento. __DATE__ é resolvida por um const char* no formato MMM DD YYYY, com o mês com três caracteres em inglês, dia com dois dígitos, completado com espaço, e ano com quatro dígitos. __TIME__ é resolvida por um const char* no formato HH:MM:SS, com o formato de horas de 24 horas.

Vamos a um exemplo:

printf("Macro Test v2.1. Build: %s\n", __DATE__);

Que retorna a saída:

Macro Test v2.1. Build: Jan 26 2021

Que pode ser muito útil para obter a data de compilação do binário em questão. Prática que facilita na definição de qual versão exatamente o programa estava sendo executado em ambiente produtivo.

Através desta data é possível efetuar buscas das versões afetadas no sistema de gestão de código fonte, por exemplo no git, subversion, cvs, etc, reduzindo o tempo necessário para determinar a causa raíz de um problema.

__STDC__, __STDC_VERSION__ e __cplusplus

__STDC__ resolve como 1, numérico, caso o compilador seja padrão ISO C. Já __STDC__VERSION__ somente estará definido caso o compilador esteja no modo C ANSI, e neste caso conterá a versão do build do compilador. O mesmo se aplica com a macro __cplusplus para C++.

Na prática são ótimas ferramentas para entender se o compilador está no modo C ANSI ou C++.

#ifdef __STDC_VERSION__
#include <stdio.h>
#endif
#ifdef __cplusplus
#include <iostream>
#endif
int main() {
    #ifdef __STDC_VERSION__
        printf("C ANSI compiler version: %d", __STDC_VERSION__);
    #endif

    #ifdef __cplusplus
        std::cout << "C++ compiler version: " << __cplusplus << std::endl;
    #endif
}

O código acima detecta se o compilador usado para fazer a pré-compilação está no modo C, ou C++. Note que o mesmo compilador pode entregar resultados diferentes.

Conhecer o compilador facilita no desenvolvimento de bibliotecas que podem ser compiladas para diversos usos, facilitando na redução da quantidade de código, e reduzindo o custo total de manutenção.