C ++项目组织(带有gtest,cmake和doxygen)


123

我是编程的新手,所以我决定从用C ++创建一个简单的矢量类开始。但是,我想从一开始就养成良好的习惯,而不是稍后再尝试修改我的工作流程。

我目前只有两个文件vector3.hppvector3.cpp。随着我对一切的熟悉,这个项目将开始慢慢发展(使它更多地成为通用的线性代数库),因此我想采用“标准”项目布局,以使以后的生活更轻松。因此,环顾四周后,我发现了两种组织hpp和cpp文件的方法,第一种是:

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

第二个是:

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

您会推荐哪个?为什么?

其次,我想使用Google C ++测试框架对我的代码进行单元测试,因为它似乎很容易使用。您是否建议将此与我的代码捆绑在一起,例如在inc/gtestcontrib/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外观如何,以便它既可以构建库又可以构建库和测试?我也看到了很多项目都有一个buildbin目录。构建是否发生在构建目录中,然后二进制文件移到bin目录中?测试的二进制文件和库是否位于同一位置?还是将其结构如下所示更有意义:

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

我也想用doxygen记录我的代码。是否有可能使它自动与cmake和make一起运行?

很抱歉有这么多问题,但是我还没有找到一本关于C ++的书,可以令人满意地回答这类问题。


6
很好的问题,但我认为这不适合Stack Overflow的Q&A格式。我对答案很感兴趣。+1&fav
Luchian Grigore 2012年

1
这些都是巨大的问题。最好将其拆分为几个较小的问题,并相互链​​接。无论如何要回答最后一部分:使用CMake,您可以选择在src目录的内部和外部进行构建(我建议在外部)。是的,您可以将doxygen与CMake自动配合使用。
mistapink 2012年

Answers:


84

C ++构建系统有点荒唐,该项目越老,您会发现越奇怪的东西,因此出现许多问题也就不足为奇了。我将尝试一个一个地解决这些问题,并提及有关构建C ++库的一些常规事项。

在目录中分隔头文件和cpp文件。仅当您要构建应该用作库而不是实际应用程序的组件时,这才是必需的。标头是用户与您提供的产品进行交互的基础,必须进行安装。这意味着它们必须位于子目录中(没有人希望许多头文件都以top结尾/usr/include/),并且您的头文件必须能够在此类设置中包含自身。

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

效果很好,因为可以计算出包含路径,并且您可以对安装目标使用简单的遍历。

捆绑依赖项:我认为这很大程度上取决于构建系统查找和配置依赖项的能力,以及代码对单个版本的依赖程度。它还取决于用户的能力以及在其平台上安装依赖项的难易程度。CMake随附find_packageGoogle测试脚本。这使事情变得容易得多。我只会在必要时进行捆绑,否则会避免捆绑。

如何构建:避免源代码构建。CMake简化了源代码构建,使生活变得更加轻松。

我想您还想使用CTest对您的系统运行测试(它还附带了对GTest的内置支持)。目录布局和测试组织的重要决定将是:您是否最终完成了子项目?如果是这样,则在设置CMakeList时需要做更多的工作,并且应该将子项目拆分为多个子目录,每个子目录都有其自己的文件includesrc文件。甚至他们自己的doxygen都可以运行和输出(可以组合多个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 上的属性。


2
很好的答案,但是我仍然不清楚为什么您需要将头文件放在另一个以项目命名的子目录中:“ / prj / include / prj / foo.hpp”,这对我来说似乎是多余的。为什么不只是“ /prj/include/foo.hpp”?我假设您将有机会在安装时重新调整安装目录,因此在安装时会得到<INSTALL_DIR> /include/prj/foo.hpp,还是在CMake下很难?
William Payne

6
@William使用CPack实际上很难做到。另外,您的包含在内部源文件中的外观如何?如果它们只是已安装版本“ / usr / include / prj /”上的“ header.hpp”,则需要包含在包含路径中,而不仅仅是“ / usr / include”。
pmr

37

首先,有一些目录的常规名称是您不能忽略的,它们基于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以及某些测试文件夹)外包构建的“构建”目录是所有文件夹,其中仅包含在构建过​​程中生成的文件。因此,只需备份或更好地将src文件夹保存在版本控制系统/服务器(例如Git或SVN)下。

当在目标系统上安装头文件时(如果最终要分发库),CMake有一个用于安装文件的命令(隐式创建“安装”目标,执行“进行安装”)您可以用来将所有标题放入/usr/include/目录中。为此,我仅使用以下cmake宏:

# custom macro to register some headers as target for installation:
#  setup_headers("/path/to/header/something.h" "/relative/install/path")
macro(setup_headers HEADER_FILES HEADER_PATH)
  foreach(CURRENT_HEADER_FILE ${HEADER_FILES})
    install(FILES "${SRCROOT}${CURRENT_HEADER_FILE}" DESTINATION "${INCLUDEROOT}${HEADER_PATH}")
  endforeach(CURRENT_HEADER_FILE)
endmacro(setup_headers)

哪里SRCROOT是CMake的变量,我设置为src文件夹,并且INCLUDEROOT是CMake的变量,我设置到哪里,以头需要去。当然,还有许多其他方法可以做到这一点,我敢肯定我的方法不是最好的。关键是,没有理由仅将标头安装在目标系统上就拆分标头和源,因为这非常容易,尤其是对于CMake(或CPack),可以选择并将标头配置为无需将它们放在单独的目录中即可安装。这就是我在大多数库中看到的。

Quote:其次,我想使用Google C ++测试框架对我的代码进行单元测试,因为它似乎很容易使用。您是否建议将其与我的代码捆绑在一起,例如在“ inc / gtest”或“ contrib / gtest”文件夹中?如果捆绑在一起,您是否建议使用fuse_gtest_files.py脚本减少文件数量或将其保留原样?如果不捆绑在一起,该依赖关系如何处理?

不要将依赖项与您的库捆绑在一起。通常这是一个非常可怕的想法,当我一直试图建立一个能够做到这一点的库时,我总是讨厌它。它应该是您的不得已的办法,并提防陷阱。通常,人们将依赖项与他们的库捆绑在一起是因为他们针对的是糟糕的开发环境(例如Windows),或者是因为他们仅支持所讨论的库的旧版本(不赞成使用)(依赖项)。主要的陷阱是,捆绑的依赖项可能与同一个库/应用程序的已安装版本冲突(例如,捆绑了gtest,但是尝试构建您的库的人已经安装了更新(或更旧)的gtest版本,然后两者可能会发生冲突,给那个人带来非常讨厌的头痛)。因此,正如我所说,这样做需要您自担风险,我只能说是不得已。与尝试解决捆绑的依赖项和现有安装之间的冲突相比,让人们在编译您的库之前先安装一些依赖项要容易得多。

Quote:在编写测试时,通常如何组织这些测试?我当时想为每个类创建一个cpp文件(例如,test_vector3.cpp),但是全部编译成一个二进制文件,这样它们就可以轻松地一起运行?

在我看来,每个类一个cpp文件(或一组小的内聚类和函数)更为常见和实用。但是,绝对不要将它们全部编译为一个二进制文件,这样“它们可以一起运行”。那真是个坏主意。通常,在编码方面,您希望尽可能合理地进行分解。对于单元测试,您不希望一个二进制文件运行所有测试,因为这意味着您对库中任何内容所做的任何微小更改都可能导致该单元测试程序几乎完全重新编译。 ,而等待重新编译的时间仅浪费了几分钟/小时。只需遵循一个简单的方案:1个单元= 1个单元测试程序。然后,

Quote:由于gtest库通常是使用cmake和make构建的,所以我认为对我的项目也这样构建有意义吗?如果我决定使用以下项目布局:

我宁愿建议这种布局:

trunk
├── bin
├── lib
   └── project
       └── libvector3.so
       └── libvector3.a        products of installation / building
├── docs
   └── Doxyfile
├── include
   └── project
       └── vector3.hpp
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

├── src
   └── CMakeLists.txt
   └── Doxyfile.in
   └── project                 part of version-control / source-distribution
       └── CMakeLists.txt
       └── vector3.hpp
       └── vector3.cpp
       └── test
           └── test_vector3.cpp
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

├── build
└── test                        working directories for building / testing
    └── test_vector3

这里有几件事要注意。首先,src目录的子目录应镜像包含目录的子目录,这只是为了保持直观(也请尝试使子目录结构保持合理的平坦(浅),因为文件夹的深度嵌套)通常比其他事情更麻烦。其次,“ include”目录只是一个安装目录,其内容就是从src目录中选择的任何标头。

第三,CMake系统旨在分布在源子目录上,而不是作为一个CMakeLists.txt文件放在顶层。这样可以使内容保持本地化,这很好(本着将内容分解为独立部分的精神)。如果添加新的源代码,新的头文件或新的测试程序,则只需在相关子目录中编辑一个小的CMakeLists.txt文件即可,而不会影响其他任何内容。这也使您可以轻松地重组目录(CMakeList是本地的,包含在要移动的子目录中)。顶级CMakeLists应该包含大多数顶级配置,例如设置目标目录,自定义命令(或宏)以及查找系统上安装的软件包。较低级别的CMakeList应仅包含标头,源,

Quote:CMakeLists.txt的外观如何,以便它既可以构建库又可以构建库和测试?

基本的答案是,CMake允许您从“全部”中专门排除某些目标(这是您键入“ make”时构建的目标),并且您还可以创建特定的目标捆绑包。我无法在此处进行CMake教程,但是直接查找是很简单的。但是,在这种情况下,推荐的解决方案当然是使用CTest,这只是可以在CMakeLists文件中使用的另一组命令,用于注册标记为unit-的许多目标(程序)。测试。因此,CMake将所有测试放在一个特殊的构建类别中,而这正是您所要的,因此,问题已得到解决。

Quote:另外,我也看到了很多项目具有一个bin目录的构建。是否在构建目录中进行构建,然后将二进制文件移到bin目录中?测试的二进制文件和库是否位于同一位置?还是将其结构如下所示更有意义:

在源代码外部(“ out-of-source”版本)之外拥有一个构建目录确实是唯一理智的事情,这已成为当今的事实上的标准。因此,当然,就像CMake人们推荐的那样,在源目录之外,还有一个单独的“ build”目录,就像我遇到的每个程序员一样。至于bin目录,这是一个惯例,坚持下去可能是个好主意,就像我在本文开头提到的那样。

Quote:我也想用doxygen来记录我的代码。是否有可能使它自动与cmake和make一起运行?

是。这是不可能的,它很棒。根据您想要获得的幻想,有几种可能性。CMake确实具有Doxygen的模块(即find_package(Doxygen)),该模块允许您注册将在某些文件上运行Doxygen的目标。如果您想做更多花哨的事情,例如更新Doxyfile中的版本号,或自动为源文件输入日期/作者戳等,则可以使用一些CMake功夫来实现。通常,执行此操作将涉及保留一个源Doxyfile(例如,我在上面的文件夹布局中放入的“ Doxyfile.in”),该源Doxyfile具有要查找的令牌并由CMake的解析命令代替。在我的顶级CMakeLists文件中,您会找到一块这样的CMake功夫,它可以与cmake-doxygen一起做一些有趣的事情。


main.cpp应该去trunk/bin吗?
UgniusMalūkas18年

17

构建项目

我通常会赞成以下几点:

├── 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测试往往更难以创建,并且更加脆弱/难以维护。gfock棺材的一个大缺点是,它确实不能与智能指针配合使用。

对于一个巨大的问题(这可能并不真正属于SO),这是一个非常琐碎和主观的答案。

您是否建议将其与我的代码捆绑在一起,例如在“ inc / gtest”或“ contrib / gtest”文件夹中?如果捆绑在一起,您是否建议使用fuse_gtest_files.py脚本减少文件数量或保留原样?如果不捆绑在一起,该依赖性如何处理?

我更喜欢使用CMake的ExternalProject_Add模块。这避免了您必须将gtest源代码保留在存储库中或将其安装在任何地方。它会自动下载并构建在您的构建树中。

在这里查看我的回答,以了解细节

在编写测试时,通常如何组织这些测试?我当时想为每个类创建一个cpp文件(例如,test_vector3.cpp),但是全部编译成一个二进制文件,这样它们就可以轻松地一起运行?

好计划。


建造

我是CMake的粉丝,但是与您的测试相关问题一样,SO可能不是针对此类主观问题征求意见的最佳场所。

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将在库过时时自动重建该库。

另外,我也看到了很多项目的构建带有bin目录。是否在构建目录中进行构建,然后将二进制文件移到bin目录中?测试的二进制文件和库是否位于同一位置?

CMake建议使用“源代码外”构建,即,您在项目外部创建自己的构建目录,然后从那里运行CMake。这样可以避免使用构建文件“污染”您的源代码树,如果您使用的是vcs,这是非常理想的。

可以指定将二进制文件一旦构建就移动或复制到其他目录,或者默认情况下在其他目录中创建它们,但是通常不需要。如果需要,CMake提供了全面的方法来安装您的项目,或者使其他CMake项目轻松“查找”项目的相关文件。

关于CMake自己对查找和执行gtest测试支持,如果将gtest作为项目的一部分进行构建,这在很大程度上是不合适的。该FindGtest模块实际上是为在项目外部单独构建gtest而设计的。

CMake提供了自己的测试框架(CTest),理想情况下,每个gtest用例都将作为CTest用例添加。

然而,GTEST_ADD_TESTS通过提供宏FindGtest,以便轻松添加GTEST案件个别案件CTEST有所欠缺的,因为它并不比其他GTEST的宏工作TESTTEST_F价值-类型,参数化的测试使用TEST_PTYPED_TEST_P等等都没有得到处理。

这个问题没有我知道的简单解决方案。获取gtest案例列表的最可靠方法是使用flag执行test exe --gtest_list_tests。但是,只有在构建了exe后才能执行此操作,因此CMake无法使用此功能。这给您两个选择。CMake必须尝试解析C ++代码以推导测试的名称(如果要考虑所有gtest宏,注释掉的测试,禁用的测试,这是非常简单的),或者将测试用例手动添加到CMakeLists.txt文件。

我也想用doxygen记录我的代码。是否有可能使它自动与cmake和make一起运行?

是的-尽管我在这方面没有经验。CMake FindDoxygen为此提供了。


6

除了其他(出色的)答案之外,我还将描述我在大型项目中一直使用的结构。
我将不讨论有关Doxygen的子问题,因为我只想重复其他答案中的内容。


基本原理

为了实现模块化和可维护性,该项目被组织为一组小型单元。为了清楚起见,我们将它们命名为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 Test,最简单的方法是在源目录中是否存在其源,但是您不必自己将其实际添加到该目录中。我一直在使用该项目来自动下载它,并且将其用法包装在一个函数中,以确保即使我们有多个测试目标,也只能下载一次。

此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)

您在Gtest和cmake那里做过的不错的“ hack”!有用!:)
Tanasis
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.