133

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

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

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

第二个是:

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

你会推荐哪个,为什么?

其次,我想使用 Google C++ 测试框架对我的代码进行单元测试,因为它看起来相当容易使用。您是否建议将此与我的代码捆绑在一起,例如在inc/gtestorcontrib/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查看才能仅构建库或库和测试?此外,我还看到了很多具有一个build和一个bin目录的项目。构建是否发生在构建目录中,然后二进制文件移出到 bin 目录中?测试和库的二进制文件会在同一个地方吗?或者将其结构如下更有意义:

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

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

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

4

4 回答 4

90

C++ 构建系统有点像魔法,项目越老,你能找到的奇怪东西就越多,所以出现很多问题也就不足为奇了。我将尝试一一解决这些问题,并提及有关构建 C++ 库的一些一般事项。

分离目录中的头文件和 cpp 文件。仅当您正在构建一个应该用作库而不是实际应用程序的组件时,这才是必不可少的。您的标头是用户与您提供的产品进行交互的基础,并且必须安装。这意味着它们必须位于子目录中(没有人希望大量标题以 top-level 结尾/usr/include/),并且您的标题必须能够将自己包含在这样的设置中。

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

效果很好,因为包含路径可以解决,并且您可以使用简单的通配符来安装目标。

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

如何构建:避免源内构建。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 的配置舞蹈之后,使用 CMakeadd_custom_command添加一个 doc 目标是微不足道的。

这就是我的项目的最终结果,我也看到了一些非常相似的项目,但当然这不是万能的。

附录在某些时候,您会想要生成一个config.hpp 包含版本定义的文件,并且可能是对某个版本控制标识符(Git 哈希或 SVN 修订号)的定义。CMake 具有自动查找该信息并生成文件的模块。您可以使用 CMakeconfigure_file将模板文件中的变量替换为CMakeLists.txt.

如果您正在构建库,您还需要一个导出定义来正确区分编译器,例如__declspec在 MSVCvisibility上和 GCC/clang 上的属性。

于 2012-11-23T00:45:55.513 回答
39

首先,您不能忽略一些常规的目录名称,这些名称是基于 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 有一个用于安装文件的命令(隐式创建一个“安装”目标,执行“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我设置到 src 文件夹的 cmake 变量在哪里,并且是INCLUDEROOT我配置到标头需要去的任何地方的 cmake 变量。当然,还有很多其他的方法可以做到这一点,我敢肯定我的方法不是最好的。关键是,没有理由仅仅因为只需要在目标系统上安装头文件就将头文件和源分开,因为很容易,特别是使用 CMake(或 CPack),挑选和配置头文件以无需将它们放在单独的目录中即可安装。这是我在大多数图书馆看到的。

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

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

Quote: 在编写测试时,这些通常是如何组织的?我正在考虑为每个类(例如 test_vector3.cpp)创建一个 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 目录的子目录,这只是为了保持直观(另外,尽量保持你的子目录结构合理平坦(浅),因为文件夹的深层嵌套通常比其他任何事情都更麻烦)。其次,“include”目录只是一个安装目录,它的内容就是从 src 目录中挑选出来的任何头文件。

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

Quote: 如何将 CMakeLists.txt 看起来,以便它可以构建库或库和测试?

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

Quote: 另外,我也看到了相当多的项目有一个构建广告一个 bin 目录。构建是否发生在构建目录中,然后二进制文件移出到 bin 目录中?测试和库的二进制文件会在同一个地方吗?或者将其结构如下更有意义:

在源代码之外拥有一个构建目录(“源外”构建)确实是唯一明智的做法,这是当今事实上的标准。所以,就像 CMake 人推荐的那样,就像我遇到的每个程序员一样,肯定地,在源目录之外有一个单独的“构建”目录。至于 bin 目录,嗯,这是一个约定,坚持它可能是一个好主意,正如我在本文开头所说的那样。

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

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

于 2012-11-23T03:47:22.397 回答
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 棺材中的一个大钉子是它真的不能很好地与智能指针配合使用。

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

您是否建议将此与我的代码捆绑在一起,例如在“inc/gtest”或“contrib/gtest”文件夹中?如果捆绑,您建议使用 fuse_gtest_files.py 脚本来减少数量或文件,还是保持原样?如果没有捆绑,如何处理这种依赖关系?

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

请参阅我在此处处理细节的答案

在编写测试时,这些通常是如何组织的?我正在考虑为每个类(例如 test_vector3.cpp)创建一个 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 推荐“out-of-source”构建,即您在项目外部创建自己的构建目录并从那里运行 CMake。这可以避免使用构建文件“污染”您的源代码树,如果您使用的是 vcs,这是非常可取的。

可以指定二进制文件在构建后移动或复制到不同的目录,或者默认在另一个目录中创建它们,但通常不需要。如果需要,CMake 提供了安装项目的综合方法,或者使其他 CMake 项目可以轻松“找到”项目的相关文件。

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

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

但是,由GTEST_ADD_TESTS提供的FindGtest允许将 gtest 案例轻松添加为单个 ctest 案例的宏有点缺乏,因为它不适用于除TEST和之外的 gtest 宏TEST_F。 根本不处理使用,等的类型参数化测试。TEST_PTYPED_TEST_P

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

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

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

于 2012-11-23T01:43:46.057 回答
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 我们想要和 的包含目录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
    )
    

使用谷歌测试

对于 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)
于 2017-07-02T07:36:54.660 回答