Como sou iniciante em programação em geral, decidi começar por criar uma classe de vetor simples em C ++. No entanto, gostaria de adotar bons hábitos desde o início, em vez de tentar modificar meu fluxo de trabalho mais tarde.
Atualmente, tenho apenas dois arquivos vector3.hpp
e vector3.cpp
. Este projeto começará lentamente a crescer (tornando-o muito mais uma biblioteca de álgebra linear geral) à medida que me familiarizar com tudo, então eu gostaria de adotar um layout de projeto "padrão" para facilitar a vida mais tarde. Então, depois de olhar em volta, encontrei duas maneiras de organizar os arquivos hpp e cpp, sendo o primeiro:
project
└── src
├── vector3.hpp
└── vector3.cpp
e o segundo sendo:
project
├── inc
│ └── project
│ └── vector3.hpp
└── src
└── vector3.cpp
O que você recomendaria e por quê?
Em segundo lugar, gostaria de usar o Google C ++ Testing Framework para testar o meu código por unidade, pois parece bastante fácil de usar. Você sugere empacotá-lo com meu código, por exemplo, em uma pasta inc/gtest
ou contrib/gtest
? Se incluído, você sugere usar o fuse_gtest_files.py
script para reduzir o número ou os arquivos ou deixá-lo como está? Se não estiver agrupado, como essa dependência é tratada?
Quando se trata de escrever testes, como eles geralmente são organizados? Eu estava pensando em ter um arquivo cpp para cada classe ( test_vector3.cpp
por exemplo), mas todos compilados em um binário para que todos possam ser executados juntos facilmente?
Como a biblioteca gtest geralmente é construída usando cmake e make, eu estava pensando que faria sentido que meu projeto também fosse construído assim? Se eu decidisse usar o seguinte layout do projeto:
├── CMakeLists.txt
├── contrib
│ └── gtest
│ ├── gtest-all.cc
│ └── gtest.h
├── docs
│ └── Doxyfile
├── inc
│ └── project
│ └── vector3.cpp
├── src
│ └── vector3.cpp
└── test
└── test_vector3.cpp
Como seria a CMakeLists.txt
aparência para poder construir apenas a biblioteca ou a biblioteca e os testes? Também vi alguns projetos que possuem build
um bin
diretório e. A construção acontece no diretório de construção e os binários foram transferidos para o diretório bin? Os binários dos testes e a biblioteca morariam no mesmo local? Ou faria mais sentido estruturá-lo da seguinte maneira:
test
├── bin
├── build
└── src
└── test_vector3.cpp
Eu também gostaria de usar doxygen para documentar meu código. É possível fazer isso rodar automaticamente com o cmake e make?
Desculpe por tantas perguntas, mas não encontrei um livro em C ++ que responda satisfatoriamente a esse tipo de pergunta.
fonte
Respostas:
Os sistemas de construção C ++ são um pouco de arte negra e, quanto mais antigo o projeto, mais coisas estranhas você pode encontrar, por isso não é surpreendente que surjam muitas perguntas. Vou tentar percorrer as perguntas uma por uma e mencionar algumas coisas gerais sobre a construção de bibliotecas C ++.
Separando cabeçalhos e arquivos cpp em diretórios. Isso é essencial apenas se você estiver criando um componente que deve ser usado como uma biblioteca, em oposição a um aplicativo real. Seus cabeçalhos são a base para os usuários interagirem com o que você oferece e deve ser instalado. Isso significa que eles precisam estar em um subdiretório (ninguém quer muitos cabeçalhos terminando no nível superior
/usr/include/
) e os cabeçalhos devem poder se incluir nessa configuração.funciona bem, porque incluir caminhos funciona e você pode usar globbing fácil para instalar destinos.
Agrupando dependências: Eu acho que isso depende muito da capacidade do sistema de compilação de localizar e configurar dependências e de quão dependente é o seu código em uma única versão. Também depende da capacidade dos seus usuários e de quão fácil é a dependência para instalar na plataforma deles. O CMake vem com um
find_package
script para o Google Test. Isso facilita muito as coisas. Eu iria com o agrupamento apenas quando necessário e evitaria o contrário.Como criar: Evite compilações na fonte. O CMake facilita a criação de fontes e facilita muito a vida.
Suponho que você também queira usar o CTest para executar testes para o seu sistema (ele também vem com suporte embutido para o GTest). Uma decisão importante para o layout do diretório e a organização de teste será: Você acaba com subprojetos? Se assim for, você precisa de um pouco mais de trabalho ao configurar CMakeLists e deve dividir seus subprojetos em subdiretórios, cada um com a sua própria
include
esrc
arquivos. Talvez até o próprio doxygen seja executado e produzido (a combinação de vários projetos doxygen é possível, mas não é fácil ou bonita).Você terminará com algo assim:
Onde
Caso você tenha subcomponentes, sugiro adicionar outra hierarquia e usar a árvore acima para cada subprojeto. Então as coisas ficam complicadas, porque você precisa decidir se os subcomponentes pesquisam e configuram suas dependências ou se você faz isso no nível superior. Isso deve ser decidido caso a caso.
Doxygen: Depois que você conseguiu passar pela dança de configuração do doxygen, é trivial usar o CMake
add_custom_command
para adicionar um destino de documento.É assim que meus projetos terminam e eu já vi alguns projetos muito semelhantes, mas é claro que isso não cura tudo.
Adendo Em algum momento, você desejará gerar um
config.hpp
arquivo que contenha uma definição de versão e talvez uma definição para algum identificador de controle de versão (um hash Git ou número de revisão SVN). O CMake possui módulos para automatizar a localização dessas informações e gerar arquivos. Você pode usar o CMakeconfigure_file
para substituir variáveis em um arquivo de modelo por variáveis definidas dentro doCMakeLists.txt
.Se você estiver construindo bibliotecas, também precisará de uma definição de exportação para acertar a diferença entre compiladores, por exemplo,
__declspec
no MSVC evisibility
atributos no GCC / clang.fonte
Para começar, existem alguns nomes convencionais para diretórios que você não pode ignorar, baseados na longa tradição do sistema de arquivos Unix. Esses são:
Provavelmente, é uma boa ideia manter esse layout básico, pelo menos no nível superior.
Sobre a divisão dos arquivos de cabeçalho e arquivos de origem (cpp), ambos os esquemas são bastante comuns. No entanto, costumo preferir mantê-los juntos, é apenas mais prático nas tarefas diárias reunir os arquivos. Além disso, quando todo o código está em uma pasta de nível superior, ou seja, a
trunk/src/
pasta, você pode observar que todas as outras pastas (bin, lib, include, doc e talvez alguma pasta de teste) no nível superior, além de o diretório "build" para uma build fora da fonte, são todas as pastas que contêm nada além de arquivos gerados no processo de build. E, portanto, apenas é necessário fazer backup da pasta src, ou muito melhor, mantida em um sistema / servidor de controle de versão (como Git ou SVN).E quando se trata de instalar seus arquivos de cabeçalho no sistema de destino (se você finalmente distribuir sua biblioteca), o CMake possui um comando para instalar arquivos (cria implicitamente um destino "install", para "make install") que você pode usar para colocar todos os cabeçalhos no
/usr/include/
diretório Eu apenas uso a seguinte macro cmake para esse fim:Onde
SRCROOT
está uma variável cmake que eu defini para a pasta src eINCLUDEROOT
é a variável cmake que eu configuro para onde quer que os cabeçalhos precisem ir. Claro, existem muitas outras maneiras de fazer isso, e tenho certeza de que meu caminho não é o melhor. O ponto é que não há razão para dividir os cabeçalhos e as fontes apenas porque apenas os cabeçalhos precisam ser instalados no sistema de destino, porque é muito fácil, especialmente com o CMake (ou CPack), escolher e configurar os cabeçalhos para ser instalado sem a necessidade de tê-los em um diretório separado. E é isso que tenho visto na maioria das bibliotecas.Não agrupe dependências com sua biblioteca. Geralmente, essa é uma ideia horrível, e eu sempre odeio quando estou parada tentando construir uma biblioteca que fez isso. Deve ser o seu último recurso e cuidado com as armadilhas. Freqüentemente, as pessoas agrupam dependências em sua biblioteca porque elas visam um ambiente de desenvolvimento terrível (por exemplo, Windows) ou porque suportam apenas uma versão antiga (obsoleta) da biblioteca (dependência) em questão. A principal armadilha é que sua dependência empacotada pode colidir com versões já instaladas da mesma biblioteca / aplicativo (por exemplo, você empacotou o gtest, mas a pessoa que tenta compilar sua biblioteca já possui uma versão mais nova (ou mais antiga) do gtest já instalada e, em seguida, os dois podem entrar em conflito e dar a essa pessoa uma dor de cabeça muito desagradável). Então, como eu disse, faça por sua conta e risco, e eu diria apenas como último recurso. Pedir às pessoas que instalem algumas dependências antes de poder compilar sua biblioteca é um mal muito menor do que tentar resolver conflitos entre suas dependências agrupadas e as instalações existentes.
Um arquivo cpp por classe (ou pequeno grupo coeso de classes e funções) é mais usual e prático na minha opinião. No entanto, definitivamente, não os compile todos em um binário apenas para que "todos possam ser executados juntos". Essa é uma péssima ideia. Geralmente, quando se trata de codificação, você deseja dividir as coisas o quanto for razoável. No caso de testes de unidade, você não deseja que um binário execute todos os testes, porque isso significa que qualquer pequena alteração feita em qualquer coisa da sua biblioteca provavelmente causará uma recompilação quase total desse programa de teste de unidade , e são apenas alguns minutos / horas perdidos à espera da recompilação. Basta seguir um esquema simples: 1 unidade = 1 programa de teste de unidade. Então,
Prefiro sugerir este layout:
Algumas coisas a serem observadas aqui. Primeiro, os subdiretórios do diretório src devem espelhar os subdiretórios do diretório de inclusão, isso é apenas para manter as coisas intuitivas (tente também manter a estrutura do subdiretório razoavelmente plana (superficial), porque o aninhamento profundo de pastas geralmente é mais complicado do que qualquer outra coisa). Segundo, o diretório "include" é apenas um diretório de instalação, seu conteúdo é exatamente o que os cabeçalhos selecionam no diretório src.
Terceiro, o sistema CMake deve ser distribuído pelos subdiretórios de origem, não como um arquivo CMakeLists.txt no nível superior. Isso mantém as coisas locais, e isso é bom (no espírito de dividir as coisas em partes independentes). Se você adicionar uma nova fonte, um novo cabeçalho ou um novo programa de teste, tudo o que você precisa é editar um arquivo CMakeLists.txt pequeno e simples no subdiretório em questão, sem afetar mais nada. Isso também permite reestruturar os diretórios com facilidade (CMakeLists são locais e estão contidos nos subdiretórios que estão sendo movidos). Os CMakeLists de nível superior devem conter a maioria das configurações de nível superior, como definir diretórios de destino, comandos personalizados (ou macros) e localizar pacotes instalados no sistema. Os CMakeLists de nível inferior devem conter apenas listas simples de cabeçalhos, fontes,
A resposta básica é que o CMake permite excluir especificamente determinados destinos de "todos" (que é criado quando você digita "make") e também pode criar pacotes específicos de destinos. Não posso fazer um tutorial do CMake aqui, mas é bastante simples descobrir você mesmo. Nesse caso específico, no entanto, a solução recomendada é, obviamente, usar o CTest, que é apenas um conjunto adicional de comandos que você pode usar nos arquivos CMakeLists para registrar um número de destinos (programas) marcados como unidades. testes. Portanto, o CMake colocará todos os testes em uma categoria especial de compilações, e é exatamente isso que você solicitou, portanto, o problema foi resolvido.
Ter um diretório de compilação fora da fonte (compilação "fora da fonte") é realmente a única coisa sensata a ser feita, é o padrão de fato atualmente. Portanto, definitivamente, tenha um diretório "build" separado, fora do diretório de origem, exatamente como o pessoal do CMake recomenda, e como todo programador que eu já conheci. Quanto ao diretório bin, bem, isso é uma convenção, e provavelmente é uma boa ideia cumpri-la, como eu disse no começo deste post.
Sim. É mais do que possível, é incrível. Dependendo de como você deseja obter, existem várias possibilidades. O CMake possui um módulo para Doxygen (isto é,
find_package(Doxygen)
) que permite registrar destinos que executam o Doxygen em alguns arquivos. Se você quiser fazer coisas mais sofisticadas, como atualizar o número da versão no arquivo Doxyfile ou inserir automaticamente uma data / autor para os arquivos de origem e assim por diante, tudo isso é possível com um pouco de kung-fu do CMake. Geralmente, isso implica que você mantenha um Doxyfile de origem (por exemplo, o "Doxyfile.in" que eu coloquei no layout da pasta acima) que possui tokens a serem encontrados e substituídos pelos comandos de análise do CMake. No meu arquivo CMakeLists de nível superior , você encontrará um desses pedaços de kung-fu do CMake que faz algumas coisas sofisticadas com o cmake-doxygen juntos.fonte
main.cpp
deve ir paratrunk/bin
?Estruturando o projeto
Eu geralmente preferiria o seguinte:
Isso significa que você tem um conjunto muito claramente definido de arquivos de API para sua biblioteca, e a estrutura significa que os clientes da sua biblioteca
ao invés de menos explícito
Eu gosto da estrutura da árvore / src para coincidir com a da árvore / include, mas é realmente uma preferência pessoal. No entanto, se o seu projeto se expandir para conter subdiretórios em / include / project, geralmente ajudará a corresponder aqueles dentro da árvore / src.
Para os testes, sou a favor de mantê-los "próximos" dos arquivos que eles testam e, se você acabar com subdiretórios no / src, é um paradigma bastante fácil para outras pessoas seguirem se quiserem encontrar o código de teste de um determinado arquivo.
Teste
O Gtest é realmente simples de usar e é bastante abrangente em termos de suas capacidades. Ele pode ser usado com o gmock com muita facilidade para ampliar suas capacidades, mas minhas próprias experiências com o gmock foram menos favoráveis. Estou bastante preparado para aceitar que isso pode ter tudo a ver com minhas próprias falhas, mas os testes do gmock tendem a ser mais difíceis de criar e muito mais frágeis / difíceis de manter. Um prego grande no caixão gmock é que ele realmente não funciona bem com ponteiros inteligentes.
Esta é uma resposta muito trivial e subjetiva a uma grande pergunta (que provavelmente não pertence à SO)
Eu prefiro usar o
ExternalProject_Add
módulo do CMake . Isso evita que você tenha que manter o código-fonte gtest no seu repositório ou instalá-lo em qualquer lugar. Ele é baixado e construído na sua árvore de construção automaticamente.Veja minha resposta sobre os detalhes aqui .
Bom plano.
Construção
Sou fã do CMake, mas, como em suas perguntas relacionadas ao teste, o SO provavelmente não é o melhor lugar para pedir opiniões sobre um assunto tão subjetivo.
A biblioteca aparecerá como um destino "ProjectLibrary" e a suíte de testes como um destino "ProjectTest". Ao especificar a biblioteca como uma dependência do exe de teste, a criação do exe de teste fará com que a biblioteca seja reconstruída automaticamente se estiver desatualizada.
O CMake recomenda compilações "fora da fonte", ou seja, você cria seu próprio diretório de compilação fora do projeto e executa o CMake a partir daí. Isso evita "poluir" sua árvore de origem com arquivos de construção e é altamente desejável se você estiver usando um vcs.
Você pode especificar que os binários sejam movidos ou copiados para um diretório diferente, uma vez criados, ou que sejam criados por padrão em outro diretório, mas geralmente não há necessidade. O CMake fornece maneiras abrangentes de instalar seu projeto, se desejado, ou facilita para outros projetos do CMake "encontrar" os arquivos relevantes do seu projeto.
Com relação ao próprio suporte do CMake para localizar e executar testes gtest , isso seria inapropriado se você criar o gtest como parte do seu projeto. O
FindGtest
módulo foi realmente projetado para ser usado no caso em que o gtest foi construído separadamente fora do seu projeto.O CMake fornece sua própria estrutura de teste (CTest) e, idealmente, todos os casos de gtest seriam adicionados como um caso de CTest.
No entanto, a
GTEST_ADD_TESTS
macro fornecida porFindGtest
para permitir a fácil adição de casos gtest como casos individuais de ctest está faltando, pois não funciona para macros do gtest além deTEST
eTEST_F
. Valor- ou Type-parametrizados testes usandoTEST_P
,TYPED_TEST_P
etc. não são tratados em tudo.O problema não tem uma solução fácil que eu saiba. A maneira mais robusta de obter uma lista de casos gtest é executar o exe de teste com o sinalizador
--gtest_list_tests
. No entanto, isso só pode ser feito após a criação do exe, portanto o CMake não pode fazer uso disso. O que deixa você com duas opções; O CMake deve tentar analisar o código C ++ para deduzir os nomes dos testes (não triviais ao extremo, se você quiser levar em consideração todas as macros gtest, testes comentados, testes desativados) ou casos de teste adicionados manualmente ao Arquivo CMakeLists.txt.Sim - embora eu não tenha experiência nesta frente. O CMake fornece
FindDoxygen
para esse fim.fonte
Além das outras (excelentes) respostas, vou descrever uma estrutura que tenho usado para projetos de larga escala .
Não vou abordar a subquestão sobre o Doxygen, pois apenas repetiria o que é dito nas outras respostas.
Fundamentação
Para modularidade e manutenção, o projeto é organizado como um conjunto de pequenas unidades. Para maior clareza, vamos chamá-los de UnitX, com X = A, B, C, ... (mas eles podem ter qualquer nome geral). A estrutura de diretórios é organizada para refletir essa opção, com a possibilidade de agrupar unidades, se necessário.
Solução
O layout básico do diretório é o seguinte (o conteúdo das unidades é detalhado posteriormente):
project/CMakeLists.txt
pode conter o seguinte:e
project/GroupA/CMakeLists.txt
:e
project/GroupB/CMakeLists.txt
:Agora, para a estrutura das diferentes unidades (tomemos, por exemplo, UnitD)
Para os diferentes componentes:
.cpp
) e headers (.h
) na mesma pasta. Isso evita uma hierarquia de diretórios duplicada, facilita a manutenção. Para a instalação, não há problema (especialmente no CMake) apenas filtrar os arquivos de cabeçalho.UnitD
é permitir posteriormente incluir arquivos com#include <UnitD/ClassA.h>
. Além disso, ao instalar esta unidade, você pode apenas copiar a estrutura de diretórios como está. Observe que você também pode organizar seus arquivos de origem em subdiretórios.README
arquivo para resumir o que é a unidade e especificar informações úteis sobre ela.CMakeLists.txt
poderia simplesmente conter:lib/CMakeLists.txt
:Aqui, observe que não é necessário informar ao CMake que queremos os diretórios de inclusão
UnitA
eUnitC
, como isso já foi especificado ao configurar essas unidades. Além disso,PUBLIC
dirá a todos os destinos que dependemUnitD
que eles devem incluir automaticamente aUnitA
dependência, enquantoUnitC
não será necessário (PRIVATE
).test/CMakeLists.txt
(veja mais abaixo, se você quiser usar o GTest):Usando o GoogleTest
Para o Google Test, o mais fácil é se a fonte estiver presente em algum lugar do diretório de origem, mas você não precisará adicioná-lo lá. Eu tenho usado esse projeto para fazer o download automaticamente e envolvo seu uso em uma função para garantir que ele seja baixado apenas uma vez, apesar de termos vários destinos de teste.
Essa função CMake é a seguinte:
e quando quiser usá-lo em um dos meus destinos de teste, adicionarei as seguintes linhas ao
CMakeLists.txt
(isto é, por exemplo, acimatest/CMakeLists.txt
):fonte