Do Comando Único ao Projeto Real — Construindo com CMake

Do Comando Único ao Projeto Real — Construindo com CMake

Compilar com uma linha de g++ deixa de servir bem antes do esperado: bastam uma dúzia de arquivos e a necessidade de construir na máquina de outra pessoa. Aqui estão o primeiro CMakeLists.txt, o ciclo de configurar e construir em etapas separadas, e a organização do código em bibliotecas ligadas ao executável.
Linguagem C++

10 min de leitura

Fechamos a Fase 7 e abrimos a Fase 8, dedicada ao que transforma código que funciona em software profissional: ferramentas, qualidade e boas práticas. Começamos pelo alicerce de todo projeto C++ real — o sistema de build. Até aqui, cada exemplo foi um arquivo único compilado com uma linha de g++. Isso serviu ao aprendizado, mas está a anos-luz de um projeto de verdade, que tem dezenas de arquivos-fonte, cabeçalhos, dependências externas e configurações diferentes para depuração e produção. Compilar tudo isso à mão, lembrando a ordem e as flags, é insustentável. O CMake é a ferramenta padrão da indústria para descrever como um projeto C++ deve ser construído, de forma portável entre compiladores e sistemas operacionais. Hoje você sai do "um arquivo, uma linha" e dá o primeiro passo no mundo real da construção de software.

O problema que o CMake resolve

Imagine um projeto com um main.cpp, um calculadora.cpp (implementação) e um calculadora.h (interface). Compilá-lo à mão exigiria conhecer todos os arquivos, a ordem, as flags de padrão e otimização, e repetir isso a cada mudança. Multiplique por dezenas de arquivos e você tem um pesadelo de manutenção. Pior: a linha de comando que funciona no seu Linux com g++ não é a mesma no macOS com clang++ ou no Windows com MSVC. O CMake resolve os dois problemas: você descreve o projeto uma vez, de forma abstrata, e o CMake gera os comandos de build corretos para o ambiente de cada um.

O coração de um projeto CMake é um arquivo chamado CMakeLists.txt. Aqui está o mínimo para o projeto de três arquivos acima:

# CMakeLists.txt — descrição do projeto

# Versão mínima do CMake exigida. 3.16+ é seguro para recursos modernos.
cmake_minimum_required(VERSION 3.16)

# Nome do projeto e linguagem.
project(Calculadora LANGUAGES CXX)

# Exige C++20 e proíbe extensões não-padrão do compilador.
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Cria um executável chamado 'calc' a partir destes arquivos-fonte.
# (Cabeçalhos .h não entram aqui; são incluídos pelos .cpp.)
add_executable(calc main.cpp calculadora.cpp)

Este arquivo descreve o projeto: sua versão mínima de CMake, seu nome, o padrão da linguagem (nosso C++20 de sempre), e que há um executável calc feito de dois .cpp. Nenhuma flag de compilador crua, nenhum caminho absoluto — tudo abstrato e portável.

O ciclo de build: configurar e compilar

Com o CMakeLists.txt pronto, construir o projeto tem duas etapas. Primeiro você configura (o CMake lê a descrição e gera os arquivos de build para seu ambiente), depois você compila. A prática recomendada é fazer isso numa pasta separada (build/), para não misturar os arquivos gerados com o código-fonte:

# A partir da raiz do projeto:
cmake -S . -B build          # configura: lê CMakeLists.txt, gera build/ 
cmake --build build          # compila: constrói o executável dentro de build/
./build/calc                 # executa o programa gerado

O -S . diz "o código-fonte está aqui"; o -B build diz "gere os arquivos de build na pasta build". Depois, cmake --build build compila. Essa separação (o out-of-source build) mantém o repositório limpo — a pasta build/ pode ser apagada e regenerada a qualquer momento, e nunca vai para o controle de versão. É um hábito profissional que vale adotar desde já.

Bibliotecas: organizando código em módulos

Projetos reais separam código reutilizável em bibliotecas, não jogam tudo no executável. O CMake torna isso natural: você cria uma biblioteca com add_library e a liga ao executável com target_link_libraries. Suponha que a calculadora vire uma biblioteca reutilizável:

cmake_minimum_required(VERSION 3.16)
project(Calculadora LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Cria uma biblioteca 'mathlib' com a lógica da calculadora.
add_library(mathlib calculadora.cpp)

# Diz onde estão os cabeçalhos da biblioteca (para quem a usar poder incluí-los).
target_include_directories(mathlib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

# O executável 'calc' usa a biblioteca.
add_executable(calc main.cpp)
target_link_libraries(calc PRIVATE mathlib)   # liga a biblioteca ao executável

Agora mathlib é um módulo independente que o executável calc consome. Essa estrutura — separar lógica reutilizável em bibliotecas e ligá-las aos executáveis — é como projetos C++ reais se organizam. O PUBLIC/PRIVATE controla a propagação das propriedades (quem usa mathlib herda seus diretórios de include, por serem PUBLIC; a ligação de calc a mathlib é PRIVATE, um detalhe interno de calc). Não se prenda a esses modificadores agora; o essencial é a ideia de compor um projeto a partir de bibliotecas e executáveis ligados entre si.

A honestidade sobre o CMake

Prometo transparência, e o CMake merece uma nota franca. Ele é a ferramenta padrão da indústria — onipresente, portável, poderoso — mas tem fama merecida de ter uma sintaxe peculiar e uma curva de aprendizado íngreme; sua própria linguagem de script é idiossincrática e sua documentação histórica confundiu gerações de programadores. A boa notícia é que o "CMake moderno" (a partir da versão 3.x, com a abordagem baseada em targets que usamos acima) é muito mais limpo que o estilo antigo que você encontrará em projetos legados e tutoriais desatualizados. Meu conselho: aprenda o estilo moderno baseado em target_* (como fizemos), ignore os exemplos antigos cheios de variáveis globais, e não tente dominar tudo de uma vez — o subconjunto que vimos hoje já constrói projetos reais. À medida que precisar de mais (dependências externas, testes, instalação), você amplia. O CMake é uma ferramenta que se aprende ao longo da carreira, não numa tarde.

Compilar à mão deixa de funcionar bem antes do que se imagina: basta uma dúzia de arquivos, uma biblioteca externa e a necessidade de compilar no computador de outra pessoa. O CMake descreve o projeto em vez dos comandos, e gera o build concreto de cada ambiente. Duas coisas valem guardar desde o começo: configurar e construir são etapas distintas, e a pasta build/ separada mantém os artefatos fora do que está versionado.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Crie um projeto com três arquivos: main.cpp (que usa uma função saudacao()), saudacao.cpp (a implementação) e saudacao.h (a declaração). Escreva o CMakeLists.txt que produz um executável a partir deles.

Ver resposta

✓ Resposta: O projeto e seu CMake:

// saudacao.h
#pragma once
#include <string>
std::string saudacao(const std::string& nome);
// saudacao.cpp
#include "saudacao.h"
std::string saudacao(const std::string& nome) {
    return "Olá, " + nome + "!";
}
// main.cpp
#include <iostream>
#include "saudacao.h"
int main() {
    std::cout << saudacao("Mundo") << '\n';
    return 0;
}
# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(Saudacao LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(app main.cpp saudacao.cpp)

Exercício 2

Explique o que cada uma das duas etapas — cmake -S . -B build e cmake --build build — faz. Por que é boa prática usar uma pasta build/ separada?

Ver resposta

✓ Resposta: cmake -S . -B build é a etapa de configuração: o CMake lê o CMakeLists.txt (fonte em .) e gera, na pasta build, os arquivos de build concretos para o ambiente atual (Makefiles no Linux, projetos do Visual Studio no Windows, etc.), detectando o compilador disponível. cmake --build build é a etapa de compilação: invoca a ferramenta de build gerada para efetivamente compilar e ligar o código, produzindo o executável dentro de build. Usar uma pasta build/ separada (out-of-source build) é boa prática porque mantém todos os artefatos gerados (objetos, executáveis, caches) isolados do código-fonte — o repositório fica limpo, a pasta build/ pode ser apagada e recriada a qualquer momento sem risco, e nunca é versionada.

Exercício 3

Modifique o projeto do exercício 1 para que a saudacao seja uma biblioteca (add_library) ligada ao executável (target_link_libraries). Explique a vantagem de separar código em bibliotecas.

Ver resposta

✓ Resposta: Com biblioteca:

cmake_minimum_required(VERSION 3.16)
project(Saudacao LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_library(saudacao_lib saudacao.cpp)
target_include_directories(saudacao_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

add_executable(app main.cpp)
target_link_libraries(app PRIVATE saudacao_lib)

A vantagem de separar em bibliotecas: o código de saudacao vira um módulo independente e reutilizável, que pode ser ligado a vários executáveis ou testes sem recompilar sua lógica repetidamente; melhora a organização (cada biblioteca tem uma responsabilidade clara), acelera builds incrementais (mudar o main não recompila a biblioteca) e torna o projeto escalável — é assim que projetos grandes se estruturam, como uma composição de bibliotecas.

Exercício 4

Explique por que os arquivos de cabeçalho (.h) geralmente não são listados em add_executable/add_library, enquanto os .cpp são. O que o target_include_directories faz nesse contexto?

Ver resposta

✓ Resposta: Os .cpp são listados porque eles são as unidades de tradução que o compilador efetivamente compila em código-objeto — cada .cpp vira um .o, e o CMake precisa saber quais compilar. Os .h não são listados porque não são compilados por si sós; eles são incluídos (via #include) dentro dos .cpp, e o compilador os processa como parte da compilação de cada .cpp que os inclui. Listar cabeçalhos em add_executable seria redundante e incorreto. O target_include_directories diz ao compilador onde procurar os arquivos de cabeçalho quando encontrar um #include — ele adiciona diretórios ao caminho de busca de includes, de modo que #include "saudacao.h" seja encontrado. Com PUBLIC, esses diretórios também são propagados a qualquer target que se ligue a essa biblioteca, para que ele também ache os cabeçalhos.

Exercício 5

Discuta por que compilar um projeto grande "à mão" com g++ se torna insustentável, e liste três problemas concretos que o CMake resolve. Dê um exemplo de como o mesmo CMakeLists.txt funciona em ambientes diferentes.

Ver resposta

✓ Resposta: Compilar à mão se torna insustentável porque, com muitos arquivos, você precisa lembrar de todos os .cpp, sua ordem, as flags corretas de padrão e otimização, e recompilar tudo (ou rastrear manualmente o que mudou) a cada alteração — propenso a erros e tedioso. Três problemas concretos que o CMake resolve: primeiro, gerência de dependências de compilação — ele sabe quais arquivos recompilar quando algo muda, evitando recompilar o projeto inteiro; segundo, portabilidade — o mesmo CMakeLists.txt gera os comandos corretos para diferentes compiladores e sistemas operacionais, sem você reescrever a linha de build; terceiro, organização em módulos — permite estruturar o projeto em bibliotecas e executáveis com dependências explícitas, em vez de uma pilha de arquivos soltos. Exemplo de portabilidade: o mesmo CMakeLists.txt que, num Linux, gera Makefiles usando g++, num macOS gera build usando clang++, e num Windows gera um projeto do Visual Studio usando MSVC — o desenvolvedor de cada plataforma roda os mesmos dois comandos cmake, e cada um obtém um build nativo correto, sem que o autor do projeto precise conhecer os detalhes de cada compilador.

Comentários

Mais em Linguagem C++

Texto como Coleção — std::string Revisitada e o std::string_view
Texto como Coleção — std::string Revisitada e o std::string_view

Enxergar std::string como container muda o que se espera dele: tamanho…

Funções Anônimas e Capturas — Lambdas de Verdade
Funções Anônimas e Capturas — Lambdas de Verdade

O que distingue uma lambda de uma função comum é a captura: ela leva consigo o…

Operações Indivisíveis — std::atomic
Operações Indivisíveis — std::atomic

Quando basta um contador ou uma flag, o mutex é peso desnecessário: o atomic…