Linguagem C++ / Módulo 2: Gerenciamento de Pacotes / Bibliotecas, Pacotes e Dependências

Bibliotecas, Pacotes e Dependências

Por que este módulo existe

No capítulo sobre sistemas de build, vimos como compilar e linkar um projeto com vários arquivos próprios. Mas praticamente nenhum programa em C++ é feito só de código próprio — cedo ou tarde, você vai querer usar uma biblioteca de terceiros: um parser de JSON, uma biblioteca de rede, algo para manipular imagens.

É aqui que o C++ se comporta de forma bem diferente de linguagens mais recentes. Se você já mexeu com Python, Node.js ou Rust, está acostumado com um fluxo parecido com pip install, npm install ou cargo add: um comando, e a biblioteca já está disponível para o seu código importar. Em C++, esse fluxo não existe de forma unificada — não há um gerenciador de pacotes embutido na linguagem ou no compilador. Cada projeto (ou cada equipe) resolve esse problema à sua maneira, e é comum encontrar os três ou quatro métodos diferentes convivendo no mesmo ecossistema.

Esse módulo existe para explicar esse cenário: por que ele é assim, o que fazer manualmente quando não há alternativa, e como os gerenciadores de pacotes modernos resolvem isso de um jeito mais próximo ao de outras linguagens.

Biblioteca, pacote e dependência não são sinônimos

Antes de ir além, vale separar três termos que aparecem misturados o tempo todo.

Biblioteca é o termo mais amplo: qualquer código reutilizável que não faz parte do seu projeto principal. Já vimos, no capítulo de build, que uma biblioteca em C++ pode ser estática (.a/.lib, copiada para dentro do executável) ou dinâmica (.so/.dll/.dylib, referenciada e carregada em tempo de execução). Isso é o artefato binário — o código já compilado.

Só que um arquivo .a ou .so sozinho normalmente não é suficiente para você usar uma biblioteca. Para chamar as funções dela, o compilador também precisa dos headers (.h/.hpp) que declaram essas funções, classes e tipos — sem eles, o compilador não sabe que uma função existe, mesmo que o código dela já esteja compilado e disponível. Uma biblioteca "completa e pronta para uso", então, é normalmente um pacote com pelo menos duas partes: o artefato binário (ou o código-fonte, quando a biblioteca é distribuída para ser compilada por você) e os headers correspondentes.

Pacote é essa unidade completa e versionada: os headers, o binário (ou instruções de como gerá-lo), dados sobre qual versão é aquela, quais são as suas próprias dependências, e para quais sistemas operacionais ela foi construída. Um pacote é o que um gerenciador de pacotes instala e organiza.

Dependência é a relação: quando o seu projeto precisa de um pacote para compilar ou rodar, esse pacote é uma dependência do seu projeto. Bibliotecas frequentemente dependem de outras bibliotecas — uma biblioteca de rede pode depender de uma biblioteca de compressão, por exemplo —, formando uma árvore de dependências que precisa ser resolvida por inteiro, não só o primeiro nível.

Por que C++ não resolveu isso "de fábrica"

A pergunta que normalmente vem a seguir é: por que outras linguagens têm um pip/npm/cargo embutido, e o C++ não?

A resposta tem raiz na própria história da linguagem, que já vimos no capítulo sobre a origem do C++: ele nasceu nos anos 80, décadas antes da internet ser algo comum, e o comitê de padronização (ISO) padronizou a linguagem e a sua biblioteca padrão — não ferramentas externas como um gerenciador de pacotes. Cada compilador (GCC, Clang, MSVC) também não inclui isso, porque não é responsabilidade deles: o trabalho de um compilador termina em gerar código de máquina a partir do código-fonte e dos headers disponíveis.

Além disso, C++ compila para código de máquina nativo, específico de cada combinação de sistema operacional, arquitetura de processador e flags de compilação (padrão da linguagem, otimização, etc.). Um pacote pré-compilado para Linux x86-64 com GCC 13 não necessariamente funciona em Windows com MSVC. Um pacote Python, por comparação, geralmente é só código-fonte interpretado — não tem esse problema de compatibilidade binária entre plataformas.

O resultado é um ecossistema fragmentado, que existe hoje em várias camadas ao mesmo tempo:

  • Inclusão manual: baixar o código-fonte ou os binários de uma biblioteca e integrá-los manualmente ao seu projeto e ao seu sistema de build. Sempre funciona, mas dá trabalho e não escala — é o que vamos ver no próximo capítulo.
  • Gerenciadores de pacotes do sistema operacional: apt, brew, pacman. Instalam bibliotecas no sistema, mas a versão disponível depende do que o mantenedor da distribuição decidiu empacotar — nem sempre é a mais recente, e o processo muda de sistema para sistema.
  • Gerenciadores de pacotes específicos de C++: vcpkg e Conan, resolvem o problema pensando especificamente nas particularidades do C++ — múltiplos compiladores, múltiplas plataformas, compatibilidade — e se integram com o CMake.

O que vem a seguir

No próximo capítulo, vamos passar pelo caminho mais manual possível: baixar o código-fonte de uma biblioteca pequena direto do GitHub, organizar os headers, e ajustar o CMakeLists.txt para que o projeto encontre e linke essa biblioteca — sem nenhuma ferramenta de gerenciamento de pacotes envolvida. Entender esse caminho manual é o que vai deixar claro, por contraste, exatamente qual problema o vcpkg e o Conan resolvem para você nos capítulos seguintes.