Pré-processamento

O pré-processador cuida de resolver todas as macros disponíveis no programa. Macros são os comandos que iniciam com #, tais como #include e #define. Para o escopo dessa seção, explicaremos o uso de quatro macros: #include, #define, #ifdef e #ifndef.

Na verdade, já usamos #include algumas vezes desde o início desse livro, mas não sabiamos exatamente seu funcionamento. Basicamente, o que esse comando faz é buscar o conteúdo de um arquivo e inclui-lo (copia-lo) no local do #include. Considere o seguinte exemplo: imagine que você tem um projeto com 2 arquivos, main.cpp e sum.hpp. Esse último contém uma função auxiliar int sum(int a, int b), e você gostaria de utilizar essa função no main.cpp. Isso pode ser feito utilizando o #include:

sum.hpp

int sum(int a, int b)
{
    return a + b;
}

main.cpp

#include <iostream>
#include "sum.hpp"

int main()
{
    std::cout << sum(2, 3) << std::endl;
    return 0;
}

Note a diferença entre os dois includes: O primeiro utiliza <nome do arquivo>, enquanto o segundo utiliza "nome do arquivo". A diferença é que enquanto "" busca o arquivo à ser incluído a partir da pasta corrente (ou seja, a mesma pasta onde reside o arquivo main.cpp, nesse caso), utilizar <> faz com que o pré-processador busque o arquivo em algumas pastas pré-determinadas, como é o caso dos diretórios padrão do sistema.

Existe um problema que, usualmente, um arquivo pode depender de outro que pode já ter sido incluído previamente. Por exemplo, se existirem dois arquivos, a.hpp e b.hpp, onde a.hpp inclui b.hpp e vice versa, teriamos uma recursão de inclusões impossível de resolver. Por isso, utiliza-se include guards para evitar esse problema, como no exemplo abaixo.

Ao incluir-se o arquivo a.hpp em main.cpp, o conteúdo do arquivo a.hpp irá ser copiado. Em a.hpp, o pré-processador irá verificar a existência da macro __A_HPP. Como ela não existe (#ifndef), ela será definida (#define), e o conteúdo de a.hpp será totalmente copiado. Dessa forma, o arquivo b.hpp será incluído. Aqui, o mesmo procedimento irá ocorrer, sendo definida uma variável __B_HPP. Porém, ao incluir a.hpp de dentro de b.hpp, percebe-se que, agora, a macro __A_HPP está definida. Sendo assim, o ifndef do arquivo a.hpp percebe que não precisa processar novamente o mesmo arquivo.

main.cpp

#include "a.hpp"

int main()
{
    return 0;
}

a.hpp

#ifndef __A_HPP
#define __A_HPP

#include "b.hpp"

#endif

b.hpp

#ifndef __B_HPP
#define __B_HPP

#include "a.hpp"

#endif

O processo de inclusão de arquivos hpp é muito importante para a modularização de código. Em C++, é comum separar a declaração das funções, classes e estruturas em arquivos .hpp, e a definição das mesmas em arquivos .cc ou .cpp. Exceto em alguns casos específicos, como no uso de templates (será visto apenas em capítulos mais avançados).

Erros de pré-processamento ocorrem na primeira etapa do processo de compilação. Para exemplificar, considere o exemplo abaixo, e imagine que não existe o arquivo some_file.hpp. O erro no gcc é a.cc:1:10: fatal error: some_file.hpp: No such file or directory. Isso significa que o arquivo não existe ou que ele existe, mas não está na pasta correta para que seja encontrado. É muito importante que se tenha isso bem conhecido, visto que esse conhecimento auxilia na resolução de problemas no processo de compilação.

#include "some_file.hpp"

int main()
{
    return 0;
}

Macros também podem ser utilizadas como "funções" modificadas em tempo de compilação. Esse tema não vai ser discutido em profundidade nesse capítulo, e sugere-se não utilizar macros para executar esse tipo de rotina em tempo de compilação. Apenas a titulo de exemplo, seria possível definir a seguinte macro:

#include <iostream>
#define POW2(x) x*x

int main()
{
    std::cout << POW2(4) << std::endl;

    return 0;
}

A macro toma como parâmetro um valor x não tipado e troca o código por x*x. Para visualizar esse efeito, no gcc, é possível compilar o arquivo acima com gcc -E main.cpp, produzindo algo semelhante ao arquivo pré-processado abaixo. Note que a compilação não gerou um executável nesse caso, pois a opção -E orienta o compilador a parar o processo de compilação após a etapa de pré-processamento.

// ... Outros comentários aqui ...
int main()
{
    std::cout << 4*4 << std::endl;

    return 0;
}

Apesar de parecer útil, a utilização de macros traz vários problemas. Por exemplo, veja o que aconteceria se chamassemos POW2(4 + 1):

int main()
{
    std::cout << 4 + 1*4 + 1 << std::endl;

    return 0;
}

O resultado seria 4 + 1 * 4 + 1, ou seja, 9 (Ao invés de 25, como seria o esperado). Isso acontece por que as macros não são equivalentes à chamadas de função. Lembre-se você está apenas criando macros que copiam valores de um lado para outro. Uma possível solução para esse caso seria de adicionar parênteses à macro: #define POW2(x) (x)*(x), resultando em:

int main()
{
    std::cout << (4 + 1)*(4 + 1) << std::endl;

    return 0;
}

De qualquer forma, orienta-se a criar funções simples do C++ e, possívelmente, utilizar constexpr em casos semelhantes à esses.