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

roz*_*zzy 115 c++ doxygen cmake googletest

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

我目前只有两个文件vector3.hppvector3.cpp.随着我对一切事物越来越熟悉,这个项目将慢慢开始增长(使其更像是一般的线性代数库),因此我希望采用"标准"项目布局,以便以后更轻松.所以在环顾四周后,我发现了两种组织hpp和cpp文件的方法,第一种方法是:

project
??? src
    ??? vector3.hpp
    ??? vector3.cpp
Run Code Online (Sandbox Code Playgroud)

第二个是:

project
??? inc
?   ??? project
?       ??? vector3.hpp
??? src
    ??? vector3.cpp
Run Code Online (Sandbox Code Playgroud)

你会推荐哪个?为什么?

其次,我想使用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
Run Code Online (Sandbox Code Playgroud)

如何CMakeLists.txt才能看到它只能构建库或库和测试?我也见过很多有a build和a bin目录的项目.构建是否发生在构建目录中,然后二进制文件移出到bin目录中?测试和图书馆的二进制文件是否存在于同一个地方?或者按如下方式组织它会更有意义:

test
??? bin
??? build
??? src
    ??? test_vector3.cpp
Run Code Online (Sandbox Code Playgroud)

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

很抱歉这么多问题,但我还没有找到一本关于C++的书,它能够令人满意地回答这些问题.

pmr*_*pmr 77

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

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

??? prj
 ??? include
 ?   ??? prj
 ?       ??? header2.h
 ?       ??? header.h
 ??? src
     ??? x.cpp
Run Code Online (Sandbox Code Playgroud)

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

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

如何构建:避免源内构建.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
Run Code Online (Sandbox Code Playgroud)

哪里

  • (1)配置依赖项,平台细节和输出路径
  • (2)配置您要构建的库
  • (3)配置测试可执行文件和测试用例

如果您有子组件,我建议添加另一个层次结构,并使用上面的树为每个子项目.事情变得棘手,因为您需要确定子组件是否搜索并配置其依赖关系,或者您是否在顶层执行此操作.这应根据具体情况决定.

Doxygen:在你设法通过doxygen的配置舞蹈之后,使用CMake add_custom_command添加doc目标是微不足道的.

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

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

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

  • @William这实际上很难用CPack做.另外,您的内部源文件如何包含?如果它们只是安装版本上的"header.hpp",则"/ usr/include/prj /"需要位于包含路径中而不是"/ usr/include". (6认同)
  • 很好的答案,但我仍然不清楚为什么你需要将你的头文件放在另一个项目命名的子目录:"/ prj/include/prj/foo.hpp",这对我来说似乎是多余的.为什么不只是"/prj/include/foo.hpp"?我假设您将有机会在安装时重新安装安装目录,因此在安装时会得到<INSTALL_DIR> /include/prj/foo.hpp,或者在CMake下是否很难? (2认同)

Mik*_*son 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
Run Code Online (Sandbox Code Playgroud)

坚持这种基本布局可能是个好主意,至少在顶层是这样.

关于拆分头文件和源文件(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)
Run Code Online (Sandbox Code Playgroud)

哪里SRCROOT是我设置到src文件夹cmake的变量,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
Run Code Online (Sandbox Code Playgroud)

这里有几点需要注意.首先,你的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目录中?测试和图书馆的二进制文件是否存在于同一个地方?或者按如下方式组织它会更有意义:

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

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

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


Fra*_*ser 17

构建项目

我通常赞成以下几点:

??? CMakeLists.txt
|
??? docs/
?   ??? Doxyfile
|
??? include/
?   ??? project/
?       ??? vector3.hpp
|
??? src/
    ??? project/
        ??? vector3.cpp
        ??? test/
            ??? test_vector3.cpp
Run Code Online (Sandbox Code Playgroud)

这意味着您的库有一组非常明确定义的API文件,结构意味着您的库的客户端可以这样做

#include "project/vector3.hpp"
Run Code Online (Sandbox Code Playgroud)

而不是不那么明确

#include "vector3.hpp"
Run Code Online (Sandbox Code Playgroud)


我喜欢/ src树的结构来匹配/ include树的结构,但这确实是个人偏好.但是,如果您的项目扩展为包含/ include/project中的子目录,则通常有助于匹配/ src树中的子目录.

对于测试,我倾向于让它们"接近"他们测试的文件,如果你最终得到/ src中的子目录,那么如果他们想要找到给定文件的测试代码,那么其他人可以很容易地遵循这些范例.


测试

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

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

对于一个巨大的问题(这可能并不属于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)
Run Code Online (Sandbox Code Playgroud)

该库将显示为目标"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案例有点缺乏,因为它不适用于gtest的宏而不是TESTTEST_F. 价值-类型,参数化的使用测试TEST_P,TYPED_TEST_P等等都没有处理.

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

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

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


oLe*_*Len 5

除了其他(优秀的)答案之外,我将描述一个我一直用于相对大规模项目的结构.
我不打算解决关于Doxygen的问题,因为我只想重复其他答案中的说法.


合理

为了模块化和可维护性,该项目被组织为一组小单元.为清楚起见,我们将它们命名为UnitX,X = A,B,C ......(但它们可以有任何通用名称).然后组织目录结构以反映该选择,并且可以在必要时对单元进行分组.

基本目录布局如下(单位内容详述稍后):

project
??? CMakeLists.txt
??? UnitA
??? UnitB
??? GroupA
?   ??? CMakeLists.txt
?   ??? GroupB
?       ??? CMakeLists.txt
?       ??? UnitC
?       ??? UnitD
?   ??? UnitE
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

并且project/GroupA/CMakeLists.txt:

add_subdirectory(GroupB)
add_subdirectory(UnitE)
Run Code Online (Sandbox Code Playgroud)

并且project/GroupB/CMakeLists.txt:

add_subdirectory(UnitC)
add_subdirectory(UnitD)
Run Code Online (Sandbox Code Playgroud)

现在到不同单位的结构(让我们来看,例如,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]
Run Code Online (Sandbox Code Playgroud)

对于不同的组件:

  • 我喜欢在同一个文件夹中有source(.cpp)和headers(.h).这避免了重复的目录层次结构,使维护更容易.对于安装,只过滤头文件是没有问题的(特别是使用CMake).
  • 该目录的作用UnitD是稍后允许包含文件#include <UnitD/ClassA.h>.此外,安装此单元时,您可以按原样复制目录结构.请注意,您还可以在子目录中组织源文件.
  • 我喜欢一个README文件来总结单元的内容并指定有用的信息.
  • CMakeLists.txt 可以简单地包含:

    add_subdirectory(lib)
    add_subdirectory(test)
    
    Run Code Online (Sandbox Code Playgroud)
  • 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
                          )
    
    Run Code Online (Sandbox Code Playgroud)

    这里,请注意,这是没有必要告诉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
    )
    
    Run Code Online (Sandbox Code Playgroud)

使用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()
Run Code Online (Sandbox Code Playgroud)

然后,当我想在我的一个测试目标中使用它时,我将添加以下行CMakeLists.txt(这是上面的例子test/CMakeLists.txt):

import_gtest()
target_link_libraries(UnitDTests gtest_main gmock_main)
Run Code Online (Sandbox Code Playgroud)