Modern C++ Development

Um boilerplate para desenvolvimento em C++

John Fercher
11 min readApr 3, 2018

Introdução

C++ é uma linguagem de programação multi-paradigma, compilada e com foco em desenvolvimento de sistemas. Sendo uma linguagem antiga, não possui várias coisas interessantes que existem em linguagens modernas. E, em vários casos, existem ferramentas que são complexas de serem utilizadas em conjunto com a linguagem.

Você deve estar se perguntando. Se C++ é uma linguagem antiga e “defasada”, por que eu iria querer utilizá-la nos dias de hoje?

Para problemas onde é necessário um pouco mais de desempenho e leveza, C++ ainda é uma escolha interessante. E, enquanto Rust não resolve seu problema de possuir dezenas de pacotes que fazem coisas pela metade, C++ irá manter-se no patamar de linguagens que valem a pena possuir em sua “caixa de ferramentas”.

Recentemente, venho trabalhando em projetos com linguagens “de mais alto nível”, como: C# e Node.JS. Embora os universos de desenvolvimento web e desenvolvimento de sistemas sejam totalmente diferentes, existem coisas que podem ser levadas de um universo para o outro. Uma dessas coisas que “não existem” em C++, é uma configuração minima de projeto que possa ser seguida para desenvolvimento de sistemas mais complexos. Por isso, venho apresentar um boilerplate para a linguagem, isto é, uma casca de projeto com várias configurações que irão possibilitar que você desenvolva em C++ sem dores de cabeça, e com suporte a várias coisas interessantes.

O boilerplate irá utilizar: Git para versionamento, Cmake para compilação, GoogleTest para realização de testes unitários, TravisCI como plataforma de integração contínua, Docker para garantir que nosso pacote irá funcionar em várias distribuições Linux e Shellscript para simplificar algumas séries de comandos.

Sendo um boilerplate, você estará apto a baixar o projeto e desenvolver em cima do mesmo, imediatamente, utilizando Cmake para rodar direto na sua máquina ou Docker para rodar em um container. Só será necessário configurar o CI, caso você venha a utilizar.

Edit: Atualmente, essa organização onde venho trabalhando utiliza o boilerplate em diversos projetos com todas as funcionalidades apresentadas nesse artigo. E, esse projeto utiliza Docker para build e testes em diferentes distros Linux.

O Boilerplate

Para quem estiver com pressa, aqui está o boilerplate:

Para quem quiser entender melhor sobre todo o processo automatizado pelo mesmo, segue uma explicação detalhada.

Organização do projeto

├── build/
├── configure.sh
├── entrypoint.sh
├── .travis.yml
├── CMakeLists.txt
├── Dockerfile
├── include/
│ └── Core.h
├── src/
│ ├── Core.cpp
│ └── Main.cpp
├── test/
│ └── CoreTests.cpp
│ └── MainTests.cpp
└── third-party/
└── googletest/

Para que seja possível integrar o TravisCI, Docker e GoogleTest em um único projeto, é necessário que os arquivos Dockerfile, .travis.yml, CMakeLists.txt, configure.sh e entrypoint.sh estejam na raiz do projeto. Vamos à explicação detalhada de cada parte do projeto.

Git

O projeto está em um repositório no serviço Github, que utiliza a tecnologia Git para realizar o armazenamento de código e controle de versão. Além de servir para guardar o projeto, temos outra motivação para sua utilização, facilitar a integração com o GoogleTest. Note que você não é obrigado a usar o Github, qualquer outra plataforma com Git serve.

Uma das formas mais fáceis de integrar um sistema com a biblioteca de testes do Google, é deixando-a como uma subpasta do seu projeto. Se fosse feito um (ctrl+C, ctrl+V) no código fonte da biblioteca dentro do diretório do projeto, também seria possível utiliza-la, porém, quando fosse necessário atualizar a biblioteca no futuro, seria necessário colar o código por cima novamente e resolver vários conflitos, definitivamente não queremos isso. Não é profissional.

Felizmente, o GoogleTest também está no Github. Com isso, podemos adiciona-lo como um submódulo do nosso repositório, que basicamente é uma referência para uma versão do repositório do Google.

Para adicionar um repositório como submódulo, basta utilizar o comando dentro do repositório principal (raiz do projeto boilerplate):

git submodule add https://github.com/google/googletest.git

Utilizando submódulos, evitamos os problemas de atualizações no futuro, visto que para atualizar o GoggleTest, apenas seria necessário executar o comando git pull dentro da pasta do submódulo. Para inicializar o repositório as coisas mudam um pouco. Temos duas opções de inicialização:

1º Opção: Quando clonar adicionar a tag (recursive)

git clone https://github.com/johnfercher/boilerplate.git --recursive

2º Opção: Após clonar o repositório, executar outros dois comandos

git clone https://github.com/johnfercher/boilerplate.git
git submodule init
git submodule update

Para mais informações sobre submódulos clique aqui.

Cmake

Como compilamos um projeto em C++? Utilizamos o g++ ou o clang? Qual versão do compilador usar? Quais tags fornecer? Temos que compilar todos os arquivos sempre? E se eu quiser que meu sistema tenha dois executáveis? E se eu quiser instalar o meu programa no Linux? E se eu quiser que meu programa tenha testes unitários???

Cmake é a resposta para todas essas perguntas. Essa ferramenta é utilizada para controlar o processo de compilação, testes e instalação. E sem brincadeiras, você realmente consegue resolver todas as questões apresentadas acima com essa ferramenta.

O projeto foi desenvolvido em três partes: main, core e tests. Segue nosso CMakeLists.txt.

Conteúdo do CMakeLists.txt. (https://github.com/johnfercher/boilerplate/blob/master/CMakeLists.txt)

O core é onde o sistema deve ser desenvolvido, sendo esse a biblioteca que carrega toda a lógica. Todos os arquivos .h e .cpp que não carreguem a declaração da função main, devem estar no core. Todos os arquivos da pasta core serão adicionados automaticamente em add_library(core …) no CMakeLists.txt através das listas SRC e INCLUDE.

A main deve possuir um único arquivo .cpp, aquele que declara a função main e chama o ponto de entrada do core. A main já está configurada em seu executável no CMakeLists.txt.

Os tests possuem um executável a parte e são um caso particular. Todo código que será utilizado para testar a lógica do core, deve estar aqui. Os tests somente possuem arquivos .cpp, e por possuírem uma função main, devem ser definidos como um executável. Todo código de teste deve ser adicionado dentro da pasta test, assim, os arquivos serão adicionados automaticamente em add_executable(tests …) no CMakeLists.txt através da lista TEST.

Caso necessite executar o projeto direto no seu sistema operacional, para compilar o mesmo basta executar os comandos dentro do boilerplate.

mkdir build
cd build
cmake ..
make

A pasta build criada pode possuir qualquer nome (apesar desse nome ser uma convenção muito utilizada), ela serve apenas para conter os arquivos gerados pelo cmake em um lugar só. Após executar o cmake, é criado um Makefile com as configurações de compilação desejadas, e executando make é compilado o programa. Após a compilação, dois executáveis serão gerados, boilerplate e tests. O primeiro sendo o sistema e o segundo os testes.

No CMakeLists.txt separamos dois casos de compilação, o default que compila sem otimização e com suporte a debug e de release que desativa a tag de debug e otimiza o executável gerado. Para compilar “para produção”, basta modificar o passo do cmake.

mkdir build
cd build
cmake -D RELEASE=ON ..
make

O resultado da compilação desse modo são os mesmos dois executáveis, porém, o boilerplate irá performar melhor.

GoogleTest

Testes unitários são parte essencial para o desenvolvimento e manutenção de sistemas. Além de garantir que algo funciona como idealizado, nos da segurança para aprimorar nosso sistema através de refatorações. Por pior que um código esteja, se o mesmo possui testes unitários, será possível refatorá-lo e transformá-lo em um código coeso.

A biblioteca utilizada para escrever nossos testes unitários é a GoogleTest, que funciona da mesma forma como em qualquer framework de testes de qualquer linguagem de programação. Dado um mecanismo composto por um conjunto de métodos, como uma calculadora com dois métodos distintos: calculator e divide.

Conteúdo do Core.cpp. (https://github.com/johnfercher/boilerplate/blob/master/src/Core.cpp)

Você pode escrever inferências sobre esses métodos, de modo a garantir que os mesmos estejam funcionando de forma correta. Por exemplo, testamos que o método divide, deve lançar uma exceção quando for divisão por zero e que quando o divisor for 1 o resultado deve ser o próprio dividendo.

Conteúdo de CoreTests.cpp. (https://github.com/johnfercher/boilerplate/blob/master/test/CoreTests.cpp)

Uma vez que um teste foi escrito para garantir que um método esteja funcionando, no futuro, caso alguém vá interagir com esse código, assim que um comportamento indevido for adicionado ao método, o teste irá passar a quebrar e o desenvolvedor será avisado que fez caquinha.

O resultado da execução dos testes deve ser retornado para o shell, para que seja possível definir ações em casos de falha ou sucesso. Para retornar o status de execução, é necessário definir uma função main para os testes, como a seguinte:

Conteúdo da MainTest.cpp. (https://github.com/johnfercher/boilerplate/blob/master/test/MainTests.cpp)

Caso todos os testes unitários passem, o executável de testes irá retornar 0. Caso algum teste falhe, o executável irá retornar 1.

Existem outras bibliotecas de testes para C++, não vou entrar na motivação por utilizar GoogleTest, você pode optar por outras libs, o que você não pode fazer é ficar sem uma dessas.

Shellscript

Chegamos a um detalhe interessante do boilerplate. Nosso amigo útil com sintaxe horrorosa. Criamos dois arquivos shellscript, um de configuração e outro de ponto de entrada. Poderíamos ter escrito esses comandos direto nos arquivos Dockerfile ou no .travis.yml, porém, estaríamos encrencados caso quiséssemos adicionar tratamentos mais específicos na execução dos comandos shell.

No arquivo configure.sh, automatizamos o processo de compilação apresentado anteriormente. Como precisamos de uma pasta para executar os passos do cmake, criamos uma função que cria a pasta caso a mesma não exista, após isso, são executados os comandos de compilação.

Conteúdo do configure.sh. (https://github.com/johnfercher/boilerplate/blob/master/configure.sh)

Antes de prosseguirmos. Note a belíssima sintaxe do nosso if, são necessários espaços entre a condicional e os colchetes. Claro, também é necessário colocar um then, após o if para iniciar o escopo. E por último, obviamente que a melhor maneira de fechar o escopo de um if, é escrever if ao contrário…

Palmas para o fi que criou essa sintaxe.

Voltando ao boilerplate. No arquivo entrypoint.sh, realizamos a execução dos testes e a execução programa desenvolvido utilizando o projeto. No script, além das funções TEST e RUN, temos uma função que verifica o último status de retorno de um comando. A função EXIT_IF_AN_ERROR_OCCURRED, chamada logo após a execução dos testes, é responsável por retornar o status para o shell.

Conteúdo do entrypoint.sh. (https://github.com/johnfercher/boilerplate/blob/master/entrypoint.sh)

Com esses scripts, fazemos com que todos os comandos apresentados na seção do cmake, tornem-se apenas bash configure.sh e bash entrypoint.sh, o que é muito melhor.

Docker

Por que estamos utilizando docker? A menos que você construa um repositório Apt, você terá problemas com dependências.

Uma pequena história que aconteceu comigo. Em 2014, fiz um projeto de TCC para o laboratório de robótica que participava como aluno (SIRLab). Em 2016, fui revisitar o projeto para meu mestrado, e… nada funcionava. Corrigi as dependências e fiz meu trabalho. Em 2018, outros alunos começaram a utilizar o mesmo projeto e adivinha? nada funcionava.

Comecei a pesquisar alternativas simples que poderiam resolver meu problema com dependências, achei o Docker interessante, pois sendo uma "VM Turbinada”, eu poderia definir uma imagem de uma distribuição Linux com meu projeto e sempre utiliza-la em qualquer lugar.

Docker funciona de uma forma parecida com Maquinas Virtuais, porém, trabalha na virtualização do sistema operacional, e não na virtualização do hardware. Essa diferença, faz com que Docker possua uma serie de vantagens, como: Possibilitar a utilização de recursos do sistema operacional hospedeiro, redução de espaço em disco, tempo de instalação e execução menores, e no nosso caso, também irá facilitar a integração contínua que vamos aplicar.

Para utilizar Docker, basta criar um Dockerfile com algumas configurações.

Conteúdo do Dockerfile. (https://github.com/johnfercher/boilerplate/blob/master/Dockerfile)

No Dockerfile, definimos que nosso container utilizará a imagem gcc:4.9, isto é, todas as dependências para desenvolvimento em C/C++ estarão presentes. Poderíamos utilizar outras imagens como debian ou ubuntu, a partir dai, teríamos acesso aos repositórios apt dessas distribuições Linux.

Na inicialização, forçamos a atualização dos pacotes e a instalação da ferramenta cmake. Após isso, copiamos todo o conteúdo da pasta do projeto para o container, adicionamos permissões de execução aos scripts e rodamos o configure.sh.

A última linha do arquivo define o script de entrada do container, que no nosso caso é entrypoint.sh, que será o responsável por rodar os testes e o sistema construído.

Para criar o container, basta executar:

cd boilerplate
sudo docker build -t boilerplate .

Para rodar os testes dentro do container, basta executar.

sudo docker run boilerplate test

Para rodar o programa dentro do container, basta executar:

sudo docker run boilerplate

TravisCI

Integração Contínua (CI), é uma pequena parte de uma série de coisas que são necessárias para a aplicação de desenvolvimento ágil. Basicamente, o CI é responsável por compilar e rodar testes em ambientes configurados. É a automatização do processo de testar um código, antes de incluí-lo em um repositório/branch principal de um projeto.

O TravisCI é um serviço onde você pode configurar seu CI. Existem diversos outros, porém, estamos utilizando esse devido o mesmo ser gratuito para projetos opensource.

O serviço será responsável por criar nosso container, compilar a aplicação, executar os testes, quebrar o CI caso ocorra algum erro e aprovar o CI caso tudo ocorra bem.

Para que o Travis consiga executar nosso projeto, definimos um arquivo .travis.yml. No arquivo, forçamos o uso de direitos de root, definimos a linguagem C++, adicionamos o Docker e por último definimos os scripts que serão executados. Note que os comandos após a tag script são os mesmos apresentados na seção que falamos sobre Docker.

Conteúdo do .travis.yml. (https://github.com/johnfercher/boilerplate/blob/master/.travis.yml)

O arquivo sozinho não faz com que o Travis consiga executar o CI, é necessário que você realize a integração do Github com o serviço via API, para aprender a realizar a integração clique aqui. Após a configuração, a cada push no repositório Git será enviado um sinal para o Travis, que irá baixar o repositório e iniciar o processo de compilação e teste. Caso tudo ocorra bem, você irá ver uma saída como essa na plataforma online.

Execução do build na plataforma travis-ci. (https://travis-ci.org/johnfercher/boilerplate)

É nessa etapa onde as coisas ganham forma. É possível configurar políticas no Github que obriguem a utilização do CI. Por exemplo, você pode obrigar que todo código que eventualmente vá para a master, passe antes por um pull request (PR). Você pode configurar para que todo PR execute um CI. E por ultimo, você pode configurar para que caso o CI falhe, o desenvolvedor não consiga realizar o merge na master. Aqui está um exemplo de um PR onde o CI falhou.

Exemplo de PR com CI quebrado no Github. (https://github.com/johnfercher/boilerplate/pull/1)

Isso é uma automação muito interessante que vem ganhando o mundo de DEV. A maioria das grandes empresas nos dias de hoje trabalham com esse tipo de processo. Algo que também deve ser dito sobre o TravisCI, é que o mesmo possui uma API de notificações. Ou seja, é possível configurar o mesmo para enviar avisos via E-mail, Slack e N outras plataformas. Para saber mais sobre as integrações possíveis clique aqui.

Conclusões

Cada parte do projeto possui sua importância para criação de um bom ambiente de desenvolvimento. Apesar de C++ ser uma linguagem antiga, ainda é possível utiliza-la em conjunto com as melhores praticas programação, porém, sua configuração não é tão simples. O boilerplate apresentado é de grande ajuda para construção de aplicações complexas, pois além de resolver problemas de dependências, o mesmo possibilita o uso de testes unitários e integração contínua.

Agradecimentos

Um obrigado a meus amigos Felipe Amaral e Manoel Stilpen por colaborarem no desenvolvimento desse artigo.

Referências

--

--

John Fercher

Tech lead at @MercadoLibre, gamer, master of science and open source contributor. More about: johnfercher.com