首页 文章

C项目组织(与gtest,cmake和doxygen)

提问于
浏览
109

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

我目前只有两个文件 vector3.hppvector3.cpp . 随着我对一切事物越来越熟悉,这个项目将慢慢开始增长(使其更像是一般的线性代数库),因此我希望采用"standard"项目布局,以便以后更轻松 . 所以在环顾四周后,我发现了两种组织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的书,它能够令人满意地回答这些问题 .

4 回答

  • 17

    C构建系统有点像黑色艺术,项目越老,你可以找到更奇怪的东西,所以很多问题出现就不足为奇了 . 我将尝试逐一介绍这些问题并提及有关构建C库的一些常规内容 .

    分离目录中的标头和cpp文件 . 只有在构建一个应该用作库而不是实际应用程序的组件时,这才是必不可少的 . 您的 Headers 是用户与您提供的内容进行交互的基础,必须安装 . 这意味着它们必须位于子目录中(没有人希望大量 Headers 最终位于顶级 /usr/include/ ),并且您的 Headers 必须能够包含这样的设置 .

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

    运行良好,因为包含路径工作,你可以使用简单的globbing安装目标 .

    捆绑依赖项:我认为这在很大程度上取决于构建系统定位和配置依赖项的能力以及您的代码在单个版本上的依赖程度 . 它还取决于用户的能力以及在其平台上安装依赖项的难易程度 . CMake附带了一个用于Google Test的 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 添加doc目标是微不足道的 .

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

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

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

  • 74

    作为入门者,有一些你不能忽视的目录的常规名称,这些是基于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")你可以使用它来放置所有 Headers 都进入 /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变量 . 当然,还有很多其他方法可以做到这一点,我相信我的方式不是最好的 . 关键是,没有理由分割 Headers 和源只是因为只需要在目标系统上安装 Headers ,因为它非常容易,特别是使用CMake(或CPack)来挑选和配置 Headers 安装时无需将它们放在单独的目录中 . 这就是我在大多数图书馆看到的 .

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

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

    Quote:谈到编写测试,如何这些一般有组织吗?我想为每个类都有一个cpp文件(例如test_vector3.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目录中挑出的 Headers .

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

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

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

    Quote:此外,我已经看到很多项目有一个构建广告bin目录 . 构建是否发生在构建目录中,然后二进制文件移出到bin目录中?测试和图书馆的二进制文件是否存在于同一个地方?或者按如下方式组织它会更有意义:

    在源代码之外有一个构建目录(“out-of-source”构建)实际上是唯一理智的事情,它现在是事实上的标准 . 所以,当然,在源目录之外有一个单独的“构建”目录,就像CMake人们推荐的那样,并且我遇到的每个程序员都有 . 至于bin目录,嗯,这是一个惯例,坚持它可能是一个好主意,正如我在本文开头所说的那样 .

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

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

  • 5

    构建项目

    我通常赞成以下几点:

    ├── 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确实易于使用,并且在功能方面相当全面 . 它可以非常容易地与_1854107一起使用以扩展其功能,但我自己使用gmock的经验不太有利 . 我用智能指针玩得很好 .

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

    您是否建议将其与我的代码捆绑在一起,例如在“inc / gtest”或“contrib / gtest”文件夹中?如果捆绑了,您是否建议使用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)
    

    该库将显示为目标“ProjectLibrary”,测试套件将显示为目标“ProjectTest” . 通过将库指定为测试exe的依赖项,构建测试exe将自动导致库在过期时重建 .

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

    CMake建议使用“out-of-source”构建,即在项目外部创建自己的构建目录并从那里运行CMake . 这样可以避免使用构建文件“污染”源树,如果您使用的是vcs,则非常需要 .

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

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

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

    但是, GTEST_ADD_TESTSFindGtest 提供的宏允许轻松添加gtest案例,因为单个ctest案例有点缺乏,因为它不包括 TESTTEST_F 之外的宏 . Value-Type-parameterised使用 TEST_PTYPED_TEST_P 等测试根本没有处理 .

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

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

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

  • 37

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

    对于谷歌测试,最简单的是如果它的源存在于你的源目录的某个地方,但是你没有使用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)
    

相关问题