Организация проекта C++ (с gtest, cmake и doxygen)


Я новичок в программировании в целом, поэтому я решил, что начну с простого векторного класса В C++. Однако я хотел бы получить хорошие привычки с самого начала, а не пытаться изменить свой рабочий процесс позже.

в настоящее время у меня есть только два файла vector3.hpp и vector3.cpp. Этот проект будет медленно расти (что делает его гораздо более общей библиотекой линейной алгебры), поскольку я все больше знаком со всем, поэтому я хотел бы принять "стандартный" проект макет, чтобы сделать жизнь проще позже. Поэтому, осмотревшись, я нашел два способа организации файлов hpp и cpp, первый из которых:

project
└── src
    ├── vector3.hpp
    └── vector3.cpp

и второе:

project
├── inc
│   └── project
│       └── vector3.hpp
└── src
    └── vector3.cpp

что бы вы порекомендовали и почему?

во-вторых, я хотел бы использовать платформу тестирования Google C++ для модульного тестирования моего кода, поскольку он кажется довольно простым в использовании. Вы предлагаете связать это с моим кодом, например, в inc/gtest или ? Если в комплекте, вы предлагаете использовать fuse_gtest_files.py скрипт для уменьшения количества или файлов, или оставить его как есть? Если не в комплекте, как эта зависимость работает?

когда дело доходит до написания тестов, как это вообще организовано? Я думал один cpp-файл для каждого класса (test_vector3.cpp например) но все они скомпилированы в один двоичный файл, чтобы их можно было легко запускать вместе?

поскольку библиотека gtest обычно строится с использованием cmake и make, я думал, что это сделает смысл в том, чтобы мой проект тоже был построен таким образом? Если я решил использовать следующий макет проекта:

├── CMakeLists.txt
├── contrib
│   └── gtest
│       ├── gtest-all.cc
│       └── gtest.h
├── docs
│   └── Doxyfile
├── inc
│   └── project
│       └── vector3.cpp
├── src
│   └── vector3.cpp
└── test
    └── test_vector3.cpp

как бы CMakeLists.txt должны выглядеть так, что он может либо построить только библиотеку или библиотеку и тесты? Также я видел довольно много проектов, которые имеют build и . Происходит ли сборка в каталоге сборки, а затем двоичные файлы перемещаются в каталог bin? Будут ли двоичные файлы для тестов и библиотеки жить в одном и том же место? Или было бы более разумно структурировать его следующим образом:

test
├── bin
├── build
└── src
    └── test_vector3.cpp

Я также хотел бы использовать doxygen для документирования моего кода. Можно ли заставить это автоматически работать с cmake и make?

извините за так много вопросов, но я не нашел книгу на C++, которая удовлетворительно отвечает на эти вопросы.

4 106

4 ответа:

системы сборки C++ - это немного черное искусство, и чем старше проект более странные вещи можно найти, так что не удивительно, что много возникают вопросы. Я попытаюсь пройти через вопросы один за другим и упомянуть некоторые общие вещи, касающиеся создания библиотек C++.

разделение заголовков и cpp файлов в каталогах. Это всего лишь важно, если вы создаете компонент, который должен использоваться как библиотека, а не фактическое приложение. Ваш заголовки являются основа для пользователей, чтобы взаимодействовать с тем, что вы предлагаете и должны быть установленный. Это означает, что они должны быть в подкаталоге (никто не хочет много заголовков в конечном итоге в верхнем уровне /usr/include/) и заголовки должны иметь возможность включать себя с такой настройкой.

└── prj
 ├── include
 │   └── prj
 │       ├── header2.h
 │       └── header.h
 └── src
     └── x.cpp

работает хорошо, потому что включают пути работать, и вы можете использовать легко подстановка для целей установки.

связывание зависимостей: я думаю, что это во многом зависит от способности система сборки для поиск и настройка зависимостей и способов зависит ваш код от одной версии. Это также зависит от того, как способны ли ваши пользователи и насколько легко установить зависимость от их платформа. CMake поставляется с find_package скрипт для Google Тест. Это делает вещи намного проще. Я бы пошел только с комплектацией при необходимости и избежать его в противном случае.

как построить: избегайте сборки в исходном коде. CMake делает из исходного кода-строит легко, и это делает жизнь намного проще.

небось вы также хотите использовать CTest для запуска тестов для вашей системы (it также поставляется со встроенной поддержкой GTest). Важное решение для макет каталога и организация тестирования будут: вы в конечном итоге с подпроекты? Если это так, вам нужно еще немного поработать при настройке CMakeLists и следует разделить ваши подпроекты на подкаталоги, каждый со своим собственный include и src файлы. Может быть, даже их собственный doxygen работает и выходы (объединение нескольких проектов, это возможно, но не легко или хорошенький.)

вы получите что-то вроде этого:

└── prj
    ├── CMakeLists.txt <-- (1)
    ├── include
    │   └── prj
    │       ├── header2.hpp
    │       └── header.hpp
    ├── src
    │   ├── CMakeLists.txt <-- (2)
    │   └── x.cpp
    └── test
        ├── CMakeLists.txt <-- (3)
        ├── data
        │   └── testdata.yyy
        └── testcase.cpp

здесь

  • (1) настраивает зависимости, специфику платформы и пути вывода
  • (2) настраивает библиотеку, которую вы собираетесь построить
  • (3) настраивает исполняемые файлы тестов и тестовые случаи

если у вас есть подкомпоненты, я бы предложил добавить еще одну иерархию и использовать дерево выше для каждого подпроекта. Тогда все становится сложно, потому что вам нужно решить, будут ли субкомпоненты искать и настраивать свои зависимости или если вы сделаете это на верхнем уровне. Это должно решаться в каждом конкретном случае.

Doxygen: после того, как вам удалось пройти через танец конфигурации помощи Doxygen, это тривиально, чтобы использовать CMake add_custom_command добавить документ.

это как мои проекты и я видел некоторые очень похожие проекты, но, конечно, это не лекарство все.

дополнительное соглашение в какой-то момент Вы захотите создать config.hpp файл, содержащий определение версии и, возможно, определение для некоторой версии идентификатор элемента управления (Git-хэш или номер версии SVN). С CMake имеет модули для автоматизации поиска этой информации и создания файлы. Вы можете использовать CMake configure_file для замены переменных в файл шаблона с переменными, определенными внутри CMakeLists.txt.

если вы строите библиотеки вам также потребуется экспорта определить получите разницу между компиляторами правильно, например __declspec на MSVC и visibility атрибуты на GCC / clang.

в качестве стартера, есть некоторые обычные имена для каталогов, которые вы не можете игнорировать, они основаны на давней традиции с файловой системой Unix. Это:

trunk
├── bin     : for all executables (applications)
├── lib     : for all other binaries (static and shared libraries (.so or .dll))
├── include : for all header files
├── src     : for source files
└── doc     : for documentation

это, вероятно, хорошая идея, чтобы придерживаться этого базового макета, по крайней мере на верхнем уровне.

о разделении заголовочных файлов и исходных файлов (cpp), обе схемы довольно распространены. Тем не менее, я предпочитаю держать их вместе, это просто более практично в повседневных задачах, чтобы иметь файлы вместе. Кроме того, когда весь код находится в одной папке верхнего уровня, т. е. trunk/src/ папка, вы можете заметить, что все другие папки (bin, lib, include, doc и, возможно, некоторые тестовые папки) на верхнем уровне, в дополнение к каталогу "build" для сборки вне источника, все папки, которые содержат не более чем файлы, созданные в процессе сборки. И таким образом, только папка src должна быть скопирована или гораздо лучше сохранена под системой / сервером управления версиями (например, Git или SVN).

и когда дело доходит до установки ваших заголовочных файлов в целевой системе (если вы хотите в конечном итоге распространять свою библиотеку), Ну, CMake имеет команду для установки файлов (неявно создает цель "install", чтобы сделать "make install"), которую вы можете использовать, чтобы поместить все заголовки в

структурирование проекта

я бы вообще за следующее:

├── CMakeLists.txt
|
├── docs/
│   └── Doxyfile
|
├── include/
│   └── project/
│       └── vector3.hpp
|
├── src/
    └── project/
        └── vector3.cpp
        └── test/
            └── test_vector3.cpp

это означает, что у вас есть очень четко определенный набор файлов API для вашей библиотеки, а структура означает, что клиенты вашей библиотеки будут делать

#include "project/vector3.hpp"

а не менее явные

#include "vector3.hpp"


Мне нравится структура дерева /src, чтобы соответствовать структуре дерева /include, но это действительно личное предпочтение. Однако, если ваш проект расширяется, чтобы содержать подкаталоги в пределах /include /project, это обычно помогает сопоставить те, которые находятся внутри дерева / src.

для тестов я предпочитаю держать их "близко" к файлам, которые они тестируют, и если вы в конечном итоге получаете подкаталоги в /src, это довольно простая парадигма для других, если они хотят найти тестовый код данного файла.


тестирование

во-вторых, я хотел бы использовать платформу тестирования Google C++ для модульного тестирования мой код, как кажется, довольно прост в использовании.

Gtest действительно прост в использовании и довольно всеобъемлющ с точки зрения его возможностей. Его можно использовать вместе с gmock очень легко расширить свои возможности, но мой собственный опыт работы с gmock был менее благоприятным. Я вполне готов признать, что это вполне может быть связано с моими собственными недостатками, но тесты gmock, как правило, сложнее создавать и гораздо более хрупкие / трудные поддерживать. Большой гвоздь в гробу gmock - это то, что он действительно не играет хорошо с умными указателями.

это очень тривиальный и субъективный ответ на огромный вопрос (который, вероятно, на самом деле не принадлежит S. O.)

вы предлагаете связать это с моим кодом, например, в папке "inc/gtest" или "contrib/gtest"? Если в комплекте, вы предлагаете использовать fuse_gtest_files.py скрипт для уменьшения количества или файлов, или оставить его как есть? Если не в комплекте, как это эта зависимость обрабатывается?

я предпочитаю использовать CMake ExternalProject_Add модуль. Это позволяет избежать необходимости хранить исходный код gtest в репозитории или устанавливать его в любом месте. Он загружается и встроен в дерево сборки автоматически.

посмотреть мои ответ, касающийся специфики здесь.

когда дело доходит до написания тестов, как это вообще организовано? Я думал иметь один файл cpp для каждого класс (test_vector3.cpp например) но все они скомпилированы в один двоичный файл, чтобы их можно было легко запускать вместе?

хороший план.


здание

я поклонник CMake, но, как и в ваших тестовых вопросах, S. O., вероятно, не лучшее место, чтобы спросить мнения по такому субъективному вопросу.

как бы CMakeLists.txt должен выглядеть так, чтобы он мог либо построить только библиотеку, либо библиотека и тесты?

add_library(ProjectLibrary <All library sources and headers>)
add_executable(ProjectTest <All test files>)
target_link_libraries(ProjectTest ProjectLibrary)

библиотека появится в качестве целевого объекта "ProjectLibrary", а набор тестов-в качестве целевого объекта"ProjectTest". Указав библиотеку в качестве зависимости от тестового exe, построение тестового exe автоматически приведет к перестройке библиотеки, если она устарела.

также я видел довольно много проектов,которые имеют каталог build ad a bin. Происходит ли сборка в каталоге сборки, а затем в двоичных файлах переехал в каталог bin? Будут ли двоичные файлы для тестов и библиотека жить в одном и том же месте?

CMake рекомендует сборки "вне источника", т. е. вы создаете свой собственный каталог сборки вне проекта и запускаете CMake оттуда. Это позволяет избежать "загрязнения" вашего исходного дерева файлами сборки и очень желательно, если вы используете vcs.

вы можете укажите, что двоичные файлы перемещаются или копируются в другой каталог один раз построенный, или что они создаются по умолчанию в другом каталоге, но в этом, как правило, нет необходимости. CMake предоставляет комплексные способы установки вашего проекта, если это необходимо, или сделать его легким для других проектов CMake, чтобы "найти" соответствующие файлы вашего проекта.

что касается самого CMake поддержка поиска и выполнения тестов gtest, это было в значительной степени неуместно если вы строите gtest как часть вашего проекта. Элемент FindGtest модуль предназначен для применения в случае, когда gtest был построен отдельно, вне вашего проекта.

CMake предоставляет свою собственную тестовую структуру (CTest), и в идеале каждый случай gtest будет добавлен как CTest case.

однако GTEST_ADD_TESTS макрос предоставлен FindGtest чтобы легко добавлять случаи gtest в качестве отдельных случаев ctest несколько не хватает в том, что он не работает для макросов gtest, кроме TEST и TEST_F. Value - или тип-параметрировать тесты с использованием TEST_P,TYPED_TEST_P и т. д. не обрабатываются вообще.

проблема не имеет простого решения, о котором я знаю. Самый надежный способ получить список случаев gtest-это выполнить тест exe с флагом --gtest_list_tests. Однако это можно сделать только после сборки exe, поэтому CMake не может использовать это. Что оставляет вам два варианта; CMake должен попытаться проанализировать код C++, чтобы вывести имена тестов (нетривиальные в крайнем случае, если вы хотите принять во внимание все макросы gtest, закомментированные тесты, отключенные тесты) или тестовые случаи добавляются вручную в списки CMakeLists.txt-файл.

я также хотел бы использовать doxygen для документирования моего кода. Можно ли заставить это автоматически работать с cmake и make?

да - хотя у меня нет опыта на этом фронте. CMake обеспечивает FindDoxygen для этой цели.

в дополнение к другим (отличным) ответам, я собираюсь описать структуру, которую я использую для относительно крупные проектов.
Я не собираюсь обращаться к подзапросу о Доксигене, так как я бы просто повторил то, что сказано в других ответах.


обоснование

для модульности и ремонтопригодности, проект организован как набор небольших блоков. Для ясности назовем их UnitX, с X = A, B, C,... (но они могут иметь любое общее название). Затем структура каталогов организуется таким образом, чтобы отразить этот выбор, с возможностью группировать единицы измерения, если это необходимо.

решение

основной макет каталога выглядит следующим образом (содержание единиц подробно описано ниже):

project
├── CMakeLists.txt
├── UnitA
├── UnitB
├── GroupA
│   └── CMakeLists.txt
│   └── GroupB
│       └── CMakeLists.txt
│       └── UnitC
│       └── UnitD
│   └── UnitE

project/CMakeLists.txt может содержать следующие элементы:

cmake_minimum_required(VERSION 3.0.2)
project(project)
enable_testing() # This will be necessary for testing (details below)

add_subdirectory(UnitA)
add_subdirectory(UnitB)
add_subdirectory(GroupA)

и project/GroupA/CMakeLists.txt:

add_subdirectory(GroupB)
add_subdirectory(UnitE)

и project/GroupB/CMakeLists.txt:

add_subdirectory(UnitC)
add_subdirectory(UnitD)

теперь к структуре из различных единиц измерения (возьмем, например, UnitD)

project/GroupA/GroupB/UnitD
├── README.md
├── CMakeLists.txt
├── lib
│   └── CMakeLists.txt
│   └── UnitD
│       └── ClassA.h
│       └── ClassA.cpp
│       └── ClassB.h
│       └── ClassB.cpp
├── test
│   └── CMakeLists.txt
│   └── ClassATest.cpp
│   └── ClassBTest.cpp
│   └── [main.cpp]

к различным компонентам:

  • мне нравится источника (.cpp) и заголовки (.h) в той же папке. Это позволяет избежать дублирования иерархии каталогов, упрощает обслуживание. Для установки, это не проблема (особенно с CMake), чтобы просто фильтровать заголовочные файлы.
  • роль каталога UnitD в дальнейшем разрешить включение файлов с #include <UnitD/ClassA.h>. Также, при установке этого устройства вы можете просто скопировать структуру каталогов как есть. Обратите внимание, что вы также можете организовать исходные файлы в подкаталогах.
  • мне нравится README файл для подведения итогов работы устройства и указания полезной информации о нем.
  • CMakeLists.txt может просто содержать:

    add_subdirectory(lib)
    add_subdirectory(test)
    
  • lib/CMakeLists.txt:

    project(UnitD)
    
    set(headers
        UnitD/ClassA.h
        UnitD/ClassB.h
        )
    
    set(sources
        UnitD/ClassA.cpp
        UnitD/ClassB.cpp    
        )
    
    add_library(${TARGET_NAME} STATIC ${headers} ${sources})
    
    # INSTALL_INTERFACE: folder to which you will install a directory UnitD containing the headers
    target_include_directories(UnitD
                               PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
                               PUBLIC $<INSTALL_INTERFACE:include/SomeDir>
                               )
    
    target_link_libraries(UnitD
                          PUBLIC UnitA
                          PRIVATE UnitC
                          )
    

    здесь обратите внимание, что не нужно говорить CMake, что мы хотим включить каталоги ибо UnitA и UnitC, как это уже было указано при настройке этих подразделений. Кроме того,PUBLIC расскажу все цели, которые зависят от UnitD что они должны автоматически включать UnitA зависимость, в то время как UnitC не требуется, то (PRIVATE).

  • test/CMakeLists.txt (смотрите ниже, если вы хотите использовать GTest для этого):

    project(UnitDTests)
    
    add_executable(UnitDTests
                   ClassATest.cpp
                   ClassBTest.cpp
                   [main.cpp]
                   )
    
    target_link_libraries(UnitDTests
                          PUBLIC UnitD
    )
    
    add_test(
            NAME UnitDTests
            COMMAND UnitDTests
    )
    

С Помощью GoogleTest

Для теста Google, самый простой, если его источник присутствует где-то в вашем исходном каталоге, но вам не нужно добавлять его туда самостоятельно. Я использую этот проект чтобы загрузить его автоматически, и я обернуть его использование в функцию, чтобы убедиться, что он загружается только один раз, хотя у нас есть несколько тестовых целей.

эта функция CMake является следующим:

function(import_gtest)
  include (DownloadProject)
  if (NOT TARGET gmock_main)
    include(DownloadProject)
    download_project(PROJ                googletest
                     GIT_REPOSITORY      https://github.com/google/googletest.git
                     GIT_TAG             release-1.8.0
                     UPDATE_DISCONNECTED 1
                     )
    set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) # Prevent GoogleTest from overriding our compiler/linker options when building with Visual Studio
    add_subdirectory(${googletest_SOURCE_DIR} ${googletest_BINARY_DIR} EXCLUDE_FROM_ALL)
  endif()
endfunction()

и затем, когда я хочу использовать его внутри одной из моих тестовых целей, я добавлю следующие строки элемент CMakeLists.txt (это для примера выше, test/CMakeLists.txt):

import_gtest()
target_link_libraries(UnitDTests gtest_main gmock_main)