C + + 项目组织(包括 gtest,cmake 和 doxygen)

我对编程还是个新手,所以我决定先用 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 + + 的书能够满意地回答这类问题。

47079 次浏览

C + + 构建系统是一门有点玄乎的艺术,而且这个项目的年代也比较久远 你就能找到更多奇怪的东西所以很多 问题接踵而至。我将试着一个接一个地回答这些问题,并提到一些关于构建 C + + 库的一般事情。

分隔目录中的头文件和 cpp 文件 如果您正在构建一个应该被使用的组件,那么 作为一个库,而不是一个实际的应用程序 用户与你所提供的互动的基础,必须是 这意味着它们必须在一个子目录中(没有人希望 许多标头最终出现在顶级 /usr/include/中)和您的 头必须能够将自己包含在这样的设置中。

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

工作良好,因为包含路径工作,您可以使用简单 安装目标的接球装置。

捆绑依赖: 我认为这在很大程度上取决于 构建系统来定位和配置依赖项以及如何 代码是否依赖于单个版本,也取决于如何 你的用户是多么容易的依赖安装在他们的 CMake 为 Google 提供了一个 find_package脚本 测试。这使事情容易多了。我会用捆绑只 在必要的时候,避免这样做。

如何构建: 避免源代码内构建 简单,而且让生活变得更简单。

我想您也想使用 CTest 来为您的系统(它)运行测试 还附带了对 GTest 的内置支持) 目录布局和测试组织将是: 如果是这样,那么在设置 CMakeList 时需要做更多的工作 并且应该将您的子项目拆分为子目录,每个子目录具有 拥有 includesrc文件。甚至可能他们自己的氧气运行和 输出(结合多个氧项目是可能的,但不容易 或漂亮)。

你最终会得到这样的结果:

└── 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)配置测试可执行文件和测试用例

如果您有子组件,我建议添加另一个层次结构,并对每个子项目使用上面的树。然后事情变得棘手,因为您需要决定是否子组件搜索和配置它们的依赖关系,或者是否在顶层进行。这应该在个案的基础上作出决定。

在你设法完成了 使用 CMake add_custom_command添加一个 Doc Target.

这就是我的项目结束的方式,我也看到过一些非常类似的项目,但是当然这并不能解决所有问题。

附录 在某些时候,你会想要生成一个 config.hpp 文件,该文件包含一个版本定义,也可能包含对某个版本的定义 控件标识符(Git 散列或 SVN 修订号) 模块来自动查找该信息并生成 可以使用 CMake 的 configure_file替换 在 CMakeLists.txt中定义了变量的模板文件。

如果您正在构建库,那么还需要一个导出定义 正确处理编译器之间的差异,例如 MSVC 上的 __declspec 以及 GCC/clang 的 visibility属性。

构建项目

我一般赞成以下几点:

├── 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 测试往往更难以创建,也更脆弱/难以维护。一个很大的问题是,它确实不适合聪明的指针。

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

您是否建议将这些代码与我的代码捆绑在一起,例如放在“ inc/gtest”或“ Contrib/gtest”文件夹中?如果是绑定的,您建议使用 use _ gtest _ files.py 脚本来减少数量或文件,还是保持原样?如果没有捆绑,如何处理这个依赖项?

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

看我的 回答这里的细节问题

当涉及到写作测试时,这些测试通常是如何组织的?我想为每个类(例如 test _ vector3.cpp)创建一个 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 将自动导致在库过期时重新生成库。

此外,我还看到了相当多的项目,有一个构建广告的 bin 目录。构建是否发生在构建目录中,然后二进制文件移到 bin 目录中?用于测试的二进制文件和库是否位于同一个位置?

CMake 推荐“源外”构建,也就是说,您可以在项目之外创建自己的构建目录,并从那里运行 CMake。这样可以避免用构建文件“污染”您的源代码树,如果您使用的是 vcs,这是非常可取的。

可以指定二进制文件在构建后移动或复制到另一个目录,或者默认情况下在另一个目录中创建二进制文件,但通常不需要这样做。如果需要,CMake 提供了全面的方法来安装您的项目,或者使其他 CMake 项目更容易“查找”您的项目的相关文件。

至于 CMake 自己的 支持查找和执行 gtest 测试,如果您将 gtest 作为项目的一部分来构建,那么这在很大程度上是不合适的。FindGtest模块实际上是设计用于在项目之外单独构建 gtest 的情况。

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

然而,FindGtest提供的 GTEST_ADD_TESTS宏允许容易地添加 gtest 案例,因为单个 ctest 案例有些缺乏,因为它不适用于除 TESTTEST_F之外的 gtest 宏。使用 TEST_PTYPED_TEST_P等的 价值 -类型参数化测试根本没有处理。

据我所知,这个问题没有简单的解决办法。获得 gtest 用例列表的最健壮的方法是执行带有 --gtest_list_tests标志的测试 exe。但是,这只能在构建 exe 之后才能完成,因此 CMake 无法利用这一点。这就给您留下了两个选择; CMake 必须尝试解析 C + + 代码来推断测试的名称(如果您想考虑所有 gtest 宏、注释掉的测试、禁用的测试,那么这些名称就非常重要) ,或者将测试用例手动添加到 CMakeLists.txt 文件中。

我还想使用 doxygen 来记录我的代码。有没有可能让它自动运行 cmake 和 make?

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

作为一个初学者,有一些传统的目录名称,你不能忽视,这些是基于长期的传统与 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”) ,你可以用它把所有的头文件放到 /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)时,可以选择和配置要安装的头文件,而不必将它们放在单独的目录中。这是我在大多数图书馆看到的。

其次,我想使用 Google C + + 测试框架对我的代码进行单元测试,因为它看起来相当容易使用。您是否建议将这些代码与我的代码捆绑在一起,例如放在“ inc/gtest”或“ Contrib/gtest”文件夹中?如果是绑定的,您建议使用 use _ gtest _ files.py 脚本来减少数量或文件,还是保持原样?如果没有捆绑,如何处理这个依赖项?

不要将依赖项与库捆绑在一起。这通常是一个非常可怕的想法,我总是讨厌它,当我被困在试图建立一个库,这样做。这应该是你最后的手段,并小心陷阱。通常,人们将依赖项与库捆绑在一起,要么是因为他们的目标是糟糕的开发环境(例如 Windows) ,要么是因为他们只支持有问题的库(依赖项)的旧版本(不推荐)。主要的缺陷是你的捆绑依赖可能会与已经安装的同一个库/应用程序的版本发生冲突(例如,你捆绑了 gtest,但是试图构建你的库的人已经安装了一个更新(或更老)的 gtest 版本,那么这两者可能会发生冲突并给那个人带来非常令人头疼的问题)。所以,就像我说的,做这件事的风险由你自己承担,而且我会说,只有在万不得已的情况下才会这么做。要求人们在编译库之前安装一些依赖项比试图解决捆绑依赖项和现有安装之间的冲突要好得多。

引用: 当涉及到写作测试时,这些测试通常是如何组织的?我想为每个类(例如 test _ vector3.cpp)创建一个 cpp 文件,但是将它们全部编译成一个二进制文件,以便它们可以轻松地一起运行?

在我看来,每个类一个 cpp 文件(或者由类和函数组成的小型内聚组)更为常见和实用。但是,绝对不要把它们都编译成一个二进制文件,以便“它们可以一起运行”。这可不是个好主意。一般来说,当涉及到编码时,您希望尽可能合理地分割各个部分。在单元测试的情况下,您不希望一个二进制文件运行所有的测试,因为这意味着您对库中的任何内容所做的任何小的更改都可能导致该单元测试程序的几乎全部重新编译,而这只是等待重新编译所损失的分钟/小时。只要坚持一个简单的方案: 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 目录的子目录,这只是为了让事情保持直观(还有,尽量让你的子目录结构合理地平坦(浅) ,因为文件夹的深度嵌套通常比其他任何东西都麻烦)。其次,“ include”目录只是一个安装目录,其内容只是从 src 目录中挑选出来的任何头文件。

第三,CMake 系统应该分布在源子目录中,而不是作为顶层的一个 CMakeLists.txt 文件。这使事情局部化,这很好(本着将事情分解成独立部分的精神)。如果您添加了新的源代码、新的头文件或新的测试程序,那么您所需要做的就是在所涉及的子目录中编辑一个小而简单的 CMakeLists.txt 文件,而不影响其他任何东西。这还允许您轻松地重构目录(CMakeList 是本地的,包含在正在移动的子目录中)。顶级 CMakeList 应该包含大多数顶级配置,比如设置目标目录、自定义命令(或宏)以及查找安装在系统上的包。低级别的 CMakeList 应该只包含头、源和单元测试源的简单列表,以及将它们注册到编译目标的 cmake 命令。

报价: CMakeLists.txt 必须看起来如何,以便它可以只构建库,或者只构建库和测试?

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

报价: 我也看到了相当多的项目,有一个建设广告的 bin 目录。构建是否发生在构建目录中,然后二进制文件移到 bin 目录中?用于测试的二进制文件和库是否位于同一个位置?或者将其结构设计如下更为合理:

在源代码之外建立一个 build 目录(“ out-of-source”build)确实是唯一明智的做法,这是当今的行业标准。因此,正如 CMake 人员所推荐的,以及我遇到的每一个程序员所做的那样,在源目录之外有一个单独的“ build”目录。至于 bin 目录,这是一个惯例,正如我在本文开头所说,坚持使用它可能是一个好主意。

我也想使用 doxygen 来记录我的代码。有没有可能让它自动运行 cmake 和 make?

是的。不仅仅是可能,简直太棒了。取决于你想变得多花哨,有几种可能性。CMake 确实有一个 Doxy模块(即 find_package(Doxygen)) ,它允许您注册在某些文件上运行 Doxy的目标。如果您想要做更多花哨的事情,比如更新 Doxyfile 中的版本号,或者为源文件自动输入日期/作者标记等等,那么只要稍微使用一点 CMake,这些都是可能的。一般来说,这样做需要保留一个源 Doxyfile (例如,我放在上面文件夹布局中的“ Doxyfile.in”) ,其中包含需要查找的标记,并由 CMake 的解析命令替换。在 我的顶级 CMakeList 文件中,你会发现一个这样的 CMake 功夫片,它可以和 CMake-doxygen 一起做一些奇妙的事情。

除了其他(优秀的)答案,我将描述一个结构,我一直在使用相对 大规模项目。
我不打算讨论关于 Doxy的子问题,因为我只是重复在其他答案中所说的内容。


理由

为了模块化和可维护性,项目被组织成一组小单元。 为了清楚起见,我们将它们命名为 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 我们需要 UnitAUnitC的 include 目录,因为在配置这些单元时已经指定了这个目录。此外,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
    )
    

Using GoogleTest

For Google Test, the easiest is if its source is present in somewhere your source directory, but you don't have to actually add it there yourself. I've been using this project to download it automatically, and I wrap its usage in a function to make sure that it is downloaded only once, even though we have several test targets.

This CMake function is the following:

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)