C ++项目组织(使用gtest,cmake和doxygen)

时间:2012-11-23 00:09:57

标签: c++ cmake doxygen googletest

我是一般的编程新手所以我决定从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脚本来减少数量或文件,或保留原样?如果没有捆绑,这个依赖是如何处理的?

在编写测试时,这些测试通常如何组织?我想为每个类(例如test_vector3.cpp)创建一个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 ++的书,它能够令人满意地回答这些问题。

4 个答案:

答案 0 :(得分:75)

C ++构建系统有点黑色,而且项目越老 你可以找到更奇怪的东西,所以这并不奇怪 问题出现了。我将尝试逐一介绍这些问题并提及有关构建C ++库的一些常规内容。

分隔目录中的标头和cpp文件。这只是 如果要构建应该使用的组件,则必不可少 作为库而不是实际应用程序。你的标题是 用户与您提供的内容互动的基础 安装。这意味着他们必须在一个子目录中(没有人想要 很多标题最终都在顶级/usr/include/)和你的 标题必须能够包含这样的设置。

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

效果很好,因为包含路径可以解决问题,您可以轻松使用 用于安装目标的globbing。

捆绑依赖项:我认为这很大程度上取决于能力 用于定位和配置依赖关系的构建系统以及如何 依赖于您的代码在单个版本上。这也取决于如何 你的用户能够和他们的安装依赖程序有多容易 平台。 CMake附带了一个适用于Google的find_package脚本 测试。这使事情变得容易多了。我会选择捆绑 必要时避免使用。

如何构建:避免源内构建。 CMake使用源代码构建 容易,它使生活更容易。

我想您也想使用CTest为您的系统运行测试(它 还附带了对GTest的内置支持。一个重要的决定 目录布局和测试组织将是:你最终得到了 子项目?如果是这样,在设置CMakeLists时需要更多工作 并应将子项目拆分为子目录,每个子目录都有 拥有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来添加a是微不足道的 doc target。

这就是我的项目最终结果,我看到了一些非常相似的项目,但当然这并不能解决所有问题。

附录在某些时候,您需要生成config.hpp 包含版本定义的文件,可能是某个版本的定义 控制标识符(Git散列或SVN修订号)。 CMake有 用于自动查找信息和生成的模块 文件。您可以使用CMake的configure_file替换a中的变量 在CMakeLists.txt内定义变量的模板文件。

如果要构建库,还需要导出定义 获得正确的编译器之间的区别,例如MSVC上的__declspec 和GCC / clang的visibility属性。

答案 1 :(得分: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,也许还有一些测试文件夹)等级,除了&#34; build&#34;用于源外构建的目录,所有文件夹只包含在构建过​​程中生成的文件。因此,只需要备份src文件夹,或者更好地保存在版本控制系统/服务器(如Git或SVN)下。

当涉及到在目标系统上安装头文件时(如果你想最终分发你的库),那么,CMake有一个安装文件的命令(隐式创建一个&#34; install&#34; target,做&#34; make install&#34;)您可以使用它将所有标题放入/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是我设置为src文件夹的cmake变量,而INCLUDEROOT是cmake变量,我将其配置为标头所需的位置。当然,还有很多其他方法可以做到这一点,我确信我的方式并不是最好的。关键是,没有理由分割标题和源只是因为只需要在目标系统上安装标题,因为它非常容易,特别是使用CMake(或CPack)来挑选和配置标题安装时无需将它们放在单独的目录中。这就是我在大多数图书馆看到的。

  

引用:其次我想使用Google C ++测试框架对我的代码进行单元测试,因为它看起来相当容易使用。您是否建议将其与我的代码捆绑在一起,例如在&#34; inc / gtest&#34;或&#34; contrib / gtest&#34;夹?如果捆绑了,您是否建议使用fuse_gtest_files.py脚本来减少数量或文件,或保留原样?如果没有捆绑,这个依赖是如何处理的?

不要将依赖项与您的库捆绑在一起。这通常是一个非常可怕的想法,当我试图建立一个这样做的库时,我总是讨厌它。它应该是你的最后手段,并提防陷阱。通常,人们将依赖关系与其库捆绑在一起,因为它们针对可怕的开发环境(例如,Windows),或者因为它们仅支持所讨论的库(依赖关系)的旧(已弃用)版本。主要的缺陷是你的捆绑依赖可能会与同一个库/应用程序的已安装版本冲突(例如,你捆绑了gtest,但是试图构建你的库的人已经安装了更新版本的gtest,那么这两个人可能会发生冲突并给那个人一个非常讨厌的头痛。因此,正如我所说的,这样做需要您自担风险,我只会说作为最后的手段。要求人们在编译库之前安装一些依赖项比尝试解决捆绑的依赖项和现有安装之间的冲突要小得多。

  

引用:在编写测试时,这些通常是如何组织的?我想为每个类创建一个cpp文件(例如test_vector3.cpp)但是所有编译成一个二进制文件以便它们可以很容易地一起运行?

在我看来,每个类的一个cpp文件(或类和函数的小内聚组)更常见和实用。但是,绝对不要将它们全部编译成一个二进制文件,以便它们可以一起运行&#34;。这是一个非常糟糕的主意。通常,在编码方面,您希望尽可能地分解内容。在单元测试的情况下,您不希望一个二进制文件运行所有测试,因为这意味着您对库中的任何内容所做的任何小改动都可能导致对该单元的几乎完全重新编译 - 测试程序,等待重新编译的时间只有几分钟/小时。坚持一个简单的方案:1个单元= 1个单元测试程序。然后,使用脚本或单元测试框架(例如gtest和/或CTest)来运行所有测试程序并报告失败/成功率。

  

引用:由于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目录的子目录,这只是为了保持直观(同时,尽量保持你的子目录结构合理平坦(浅),因为文件夹的深层嵌套往往比其他任何事情更麻烦)。其次,&#34;包括&#34; directory只是一个安装目录,它的内容只是从src目录中挑出的标题。

第三,CMake系统旨在分布在源子目录上,而不是作为顶层的一个CMakeLists.txt文件。这使得事物保持本地化,这是好事(本着将事物分成独立部分的精神)。如果添加新源,新标头或新测试程序,您只需在相关子目录中编辑一个小而简单的CMakeLists.txt文件,而不会影响其他任何内容。这也允许您轻松地重构目录(CMakeLists是本地的并包含在被移动的子目录中)。顶级CMakeLists应包含大多数顶级配置,例如设置目标目录,自定义命令(或宏)以及查找系统上安装的软件包。较低级别的CMakeLists应仅包含标头,源和单元测试源的简单列表,以及将它们注册到编译目标的cmake命令。

  

引用:CMakeLists.txt如何看待它可以构建只是库或库和测试?

基本答案是,CMake允许您明确地从&#34;所有&#34;中排除某些目标。 (这是您键入&#34; make&#34;)时构建的内容,您还可以创建特定的目标包。我不能在这里做一个CMake教程,但是你可以自己找到它。但是,在这种特定情况下,推荐的解决方案当然是使用CTest,它只是一组额外的命令,您可以在CMakeLists文件中使用这些命令来注册标记为单元的多个目标(程序) - 试验。因此,CMake将把所有测试都放在一个特殊的构建类别中,这正是你所要求的,所以问题就解决了。

  

引用:我也看过很多项目都有一个构建广告的bin目录。构建是否发生在构建目录中,然后二进制文件移出到bin目录中?测试和图书馆的二进制文件是否存在于同一个地方?或者按照以下方式构建它会更有意义:

在源代码之外有一个构建目录(&#34; out-of-source&#34; build)实际上是唯一理智的事情,它现在是事实上的标准。所以,当然,有一个单独的&#34;构建&#34;目录,在源目录之外,就像CMake人推荐的那样,以及我见过的每个程序员都可以。至于bin目录,嗯,这是一个惯例,坚持它可能是个好主意,就像我在本文开头所说的那样。

  

Quote:我还想用doxygen来记录我的代码。是否可以通过cmake和make自动运行?

是。这是可能的,它很棒。根据您想要的花哨程度,有几种可能性。 CMake确实有一个Doxygen模块(即find_package(Doxygen)),它允许你注册将在某些文件上运行Doxygen的目标。如果你想做更多花哨的事情,比如更新Doxyfile中的版本号,或者自动输入源文件的日期/作者标记等等,那么可以使用一些CMake kung-fu。通常,执行此操作将涉及您保留源Doxy文件(例如,我在上面的文件夹布局中放置的&#34; Doxyfile.in&#34),其中包含要查找的令牌并由CMake解析替换命令。在my top-level CMakeLists file中,你会发现一块这样的CMake kung-fu,它与cmake-doxygen一起做了一些奇特的事情。

答案 2 :(得分: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树中的子目录。

对于测试,我赞成保持他们&#34;关闭&#34;对于他们测试的文件,如果你最终在/ src中使用子目录,那么如果他们想要查找给定文件的测试代码,其他人就可以轻松地遵循它。


测试

  

其次,我想使用Google C ++测试框架对我的代码进行单元测试,因为它看起来相当容易使用。

Gtest确实易于使用,并且在功能方面相当全面。它可以很容易地与gmock一起使用来扩展它的功能,但我自己对gmock的体验却不那么有利。我已经准备好接受这可能归结为我自己的缺点,但gmock测试往往更难创建,更脆弱/难以维护。 gmock棺材中的一个大钉子就是它与智能指针的搭配并不好用。

对于一个巨大的问题(这可能并不属于S.O。),这是一个非常微不足道的主观答案。

  

您是否建议将其与我的代码捆绑在一起,例如在&#34; inc / gtest&#34;或&#34; contrib / gtest&#34;夹?如果捆绑了,您是否建议使用fuse_gtest_files.py脚本来减少数量或文件,或保留原样?如果没有捆绑,这个依赖是如何处理的?

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

查看我的answer dealing with the specifics here

  

在编写测试时,这些测试通常如何组织?我想为每个类创建一个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)

图书馆将作为目标&#34; ProjectLibrary&#34;,以及测试套件作为目标&#34; ProjectTest&#34;。通过将库指定为测试exe的依赖项,构建测试exe将自动导致库在过期时重建。

  

此外,我看到很多项目都有一个构建广告和bin目录。构建是否发生在构建目录中,然后二进制文件移出到bin目录中?测试的二进制文件和库是否存在于同一个地方?

CMake建议&#34; out-of-source&#34;构建,即您在项目外部创建自己的构建目录并从那里运行CMake。这避免了污染&#34;您的源代码树包含构建文件,如果您正在使用vcs,则非常需要。

可以指定二进制文件在构建后被移动或复制到其他目录,或者默认情况下在另一个目录中创建它们,但通常不需要。如果需要,CMake提供了全面的方法来安装您的项目,或者让其他CMake项目能够轻松地找到&#34;找到&#34;您项目的相关文件。

关于CMake自己的support for finding and executing gtest tests,如果你将gtest构建为项目的一部分,那么这将是不合适的。 FindGtest模块实际上设计用于在项目之外单独构建gtest的情况。

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

然而,GTEST_ADD_TESTS提供的FindGtest宏允许轻松添加gtest案例作为单独的ctest案例,但有些缺乏,因为它不适用于gtest的其他宏比TESTTEST_F。使用TEST_PTYPED_TEST_P等的Value-Type-parameterised测试根本无法处理。

问题并不是我所知道的简单解决方案。获取gtest案例列表的最有效方法是使用标志--gtest_list_tests执行测试exe。但是,这只能在构建exe后才能完成,因此CMake无法使用它。这让你有两个选择; CMake必须尝试解析C ++代码以推断出测试的名称(如果你想考虑所有的gtest宏,注释掉的测试,禁用的测试,那么极端的非常重要),或者手动将测试用例添加到CMakeLists.txt文件。

  

我还想用doxygen来记录我的代码。是否可以通过cmake和make自动运行?

是的 - 虽然我没有这方面的经验。 CMake为此目的提供了FindDoxygen

答案 3 :(得分:5)

除了其他(优秀)答案之外,我将描述一个我用于相对大型项目的结构。
我不打算解决关于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)

现在来看不同单位的结构(让我们以单位D为例)

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我们想要UnitAUnitC的包含目录,因为在配置这些单位时已经指定了这些目录。此外,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,最简单的方法是,如果源代码存在于您的源目录的某个位置,但您不必自己实际添加它。 我一直在使用this project自动下载它,我将它的用法包装在一个函数中,以确保它只下载一次,即使我们有多个测试目标。

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