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

我是一般的编程新手,所以我决定从C ++开始创build一个简单的向量类。 不过,我想从一开始就接受良好的习惯,而不是稍后尝试修改我的工作stream程。

我目前只有两个文件vector3.hppvector3.cpp 。 随着我越来越熟悉这个项目,这个项目将慢慢地开始增长(使它更像一个一般的线性代数库),所以我想采用一个“标准”的项目布局,以便以后更容易。 所以在环视后我发现了两种组织hpp和cpp文件的方法,第一种是:

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

第二个是:

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

你会推荐哪个,为什么?

其次我想使用谷歌C + +testing框架unit testing我的代码,因为它似乎相当容易使用。 你是否build议把这个与我的代码捆绑在一起,例如在inc/gtest或者contrib/gtest文件夹中? 如果捆绑,你build议使用fuse_gtest_files.py脚本来减less数量或文件,或保持原样? 如果没有捆绑,这个依赖关系是如何处理的?

在编写testing时,这些通常是如何组织的? 我想每个类都有一个cpp文件(例如test_vector3.cpp ),但都编译成一个二进制文件,以便它们都可以轻松地运行在一起。

因为gtest库通常是用cmake和make构build的,所以我认为我的项目也可以这样构build? 如果我决定使用下面的项目布局:

 ├── 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立图书馆或图书馆和testing? 我也见过不less有一个build和一个bin目录的项目。 构build是否发生在构build目录中,然后将二进制文件移出到bin目录中? 考试的二进制文件和图书馆会住在同一个地方吗? 或者更有意义的构造如下:

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

我也想用doxygen来logging我的代码。 是否有可能得到这个自动与cmake运行,使?

对不起,这么多的问题,但我还没有find一本C ++的书,圆满地回答这些types的问题。

C ++构build系统是一种黑色艺术,而这个项目越老,你就可以find更奇怪的东西,所以出现很多问题并不奇怪。 我将尝试逐一解答这些问题,并提到关于构buildC ++库的一些常规事项。

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

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

运行良好,因为包含path的工作,你可以使用容易globbing安装目标。

绑定依赖关系:我认为这在很大程度上取决于构build系统定位和configuration依赖关系的能力,以及如何将代码依赖于单个版本。 这也取决于你的用户的能力如何,在他们的平台上依赖安装有多容易。 CMake为Googletesting提供了一个find_package脚本。 这使事情变得更容易。 我只会在必要的时候捆绑捆绑,否则就避免捆绑。

如何构build:避免内源构build。 CMake使得源代码构build变得简单,并且使生活变得更容易。

我想你也想使用CTest来为你的系统运行testing(它也带有GTest的内置支持)。 目录布局和testing组织的一个重要决定将是:你最终是否有子项目? 如果是这样,那么在设置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)configuration依赖关系,平台细节和输出path
  • (2)configuration你要build立的库
  • (3)configurationtesting可执行文件和testing用例

如果你有子组件,我会build议添加另一个层次结构,并为每个子项目使用上面的树。 然后事情变得棘手,因为您需要决定子组件是否search和configuration它们的依赖关系,或者是否在顶层进行。 这应该根据具体情况来决定。

Doxygen:在您设法通过doxygen的configuration舞蹈之后,使用CMake add_custom_command来添加一个doc目标是微不足道的。

这就是我的项目如何结束,我看到了一些非常类似的项目,但这当然不是全部。

附录在某些时候,您将需要生成一个config.hpp文件,其中包含一个版本定义,并可能定义了一些版本控制标识(Git散列或SVN修订版本号)。 CMake有模块来自动查找信息并生成文件。 您可以使用CMake的configure_file来将模板文件中的variablesreplace为CMakeLists.txt定义的variables。

如果您正在构build库,您还需要一个导出定义来正确地获得编译器之间的差异,例如MSVC上的__declspec和GCC / clang上的visibility属性。

作为一个初学者,有一些传统的目录名称是你不能忽视的,这些都是基于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 

坚持这个基本布局可能是一个好主意,至less在顶层。

关于分割头文件和源文件(cpp),这两种scheme是相当普遍的。 但是,我倾向于把它们放在一起,把日志文件放在一起的日常任务更加实用。 另外,当所有的代码都在一个顶层文件夹,即trunk/src/文件夹下时,你可以注意到顶层的所有其他文件夹(bin,lib,include,doc,也许是一些testing文件夹)除了用于源外构build的“构build”目录之外,所有文件夹都只包含构build过程中生成的文件。 因此,只有src文件夹需要备份,或者更好地保存在版本控制系统/服务器(如Git或SVN)下。

当你在目标系统上安装你的头文件(如果你想最终分发你的库),那么CMake有一个安装文件的命令(隐式地创build一个“安装”目标,做“安装”)您可以使用将所有标题放入/usr/include/目录。 我只是使用下面的cmakemacros来达到这个目的:

 # 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文件夹的cmakevariables, INCLUDEROOT是cmakevariables,可以将其configuration为标头需要去的任何位置。 当然,还有很多其他的方法可以做到这一点,我相信我的方式不是最好的。 问题是,没有必要将头文件和源文件分开,因为只有头文件需要安装在目标系统上,因为这非常容易,特别是使用CMake(或者CPack)来挑选和configuration头文件无需将其安装在单独的目录中即可进行安装。 这是我在大多数图书馆看到的。

Quote:其次,我想使用谷歌C + +testing框架unit testing我的代码,因为它似乎相当容易使用。 您是否build议将其与我的代码捆绑在一起,例如在“inc / gtest”或“contrib / gtest”文件夹中? 如果捆绑,你build议使用fuse_gtest_files.py脚本来减less数量或文件,或保持原样? 如果没有捆绑,这个依赖关系是如何处理的?

不要将依赖项与你的库捆绑在一起。 这通常是一个非常可怕的想法,当我试图build立一个图书馆,我一直恨它。 这应该是你的最后手段,并提防陷阱。 通常情况下,人们依赖于它们的库,或者是因为它们面向一个可怕的开发环境(比如Windows),或者是因为它们只支持一个旧的(不推荐的)版本的库(dependency)。 主要的缺点是你捆绑的依赖可能与已经安装的相同的库/应用程序的版本冲突(例如,你捆绑了gtest,但是试图构build你的库的人已经安装了一个更新版本的gtest,这两个人可能会冲突,给这个人一个非常讨厌的头痛)。 所以,正如我所说的那样,自己承担风险,我只能说是最后的手段。 要求人们在能够编译库之前安装一些依赖关系比尝试解决捆绑的依赖关系和现有安装之间的冲突要小得多。

Quote:说到写作testing,这些通常是如何组织的? 我想每个类都有一个cpp文件(例如test_vector3.cpp),但都编译成一个二进制文件,以便它们都可以轻松地运行在一起。

在我看来,每个类的一个cpp文件(或者小的粘合性的类和函数组)是比较平常和实用的。 但是,绝对不要将它们全部编译成一个二进制文件,以便“它们都可以一起运行”。 这是一个非常糟糕的主意。 一般来说,在编码方面,你想尽可能多地分割。 在unit testing的情况下,你不希望一个二进制文件运行所有的testing,因为这意味着你对库中的任何东西所做的任何小的改动都可能导致对该unit testing程序的近乎完全的重新编译,这只是几分钟/小时,等待重新编译。 只要坚持一个简单的scheme:1个单位= 1个unit testing程序。 然后,使用脚本或unit testing框架(如gtest和/或CTest)运行所有testing程序并报告失败/成功率。

Quote:由于gtest库一般是使用cmake构build和使,我想这是有道理的,我的项目也是这样构build的? 如果我决定使用下面的项目布局:

我宁愿build议这样的布局:

 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文件。 这使局部保持局部,这是很好的(把事情分解成独立的部分)。 如果你添加一个新的源代码,一个新的头文件或者一个新的testing程序,你只需要编辑一个小而简单的CMakeLists.txt文件到子目录中,而不会影响其他任何东西。 这也可以让你轻松地重构目录(CMakeLists是本地的,包含在被移动的子目录中)。 顶级CMakeLists应该包含大多数顶级configuration,例如设置目标目录,自定义命令(或macros)以及查找安装在系统上的软件包。 下层CMakeLists应该只包含简单的头文件,源文件和unit testing源列表,以及将它们注册到编译目标的cmake命令。

Quote:如何CMakeLists.txt必须看,以便它可以只build立图书馆或图书馆和testing?

基本的答案是,CMake允许你从“全部”(当你input“make”时build立的)专门排除某些目标,你也可以创build特定的目标捆绑。 我不能在这里做一个CMake的教程,但它是相当直接find自己。 然而,在这种特殊情况下,推荐的解决scheme当然是使用CTest,它只是一组额外的命令,您可以在CMakeLists文件中使用这些命令来注册许多标记为单元的目标(程序)试验。 所以,CMake会把所有的testing放在一个特殊的构build类别中,而这正是你所要求的,所以问题就解决了。

Quote:另外,我已经看到了不less有一个build立广告一个bin目录的项目。 构build是否发生在构build目录中,然后将二进制文件移出到bin目录中? 考试的二进制文件和图书馆会住在同一个地方吗? 或者更有意义的构造如下:

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

Quote:我也想用doxygen来logging我的代码。 是否有可能得到这个自动与cmake运行,使?

是。 这是可能的,它是真棒。 根据你想要得到多么奇特,有几种可能性。 CMake确实有一个用于Doxygen的模块(即find_package(Doxygen) ),它允许你注册在某些文件上运行Doxygen的目标。 如果你想做更多的花哨的事情,比如更新Doxyfile中的版本号,或者自动input源文件的date/作者标记等等,都可以使用一些CMake kung-fu。 一般来说,这样做会涉及到你保留一个源代码Doxyfile(例如,我放在上面的文件夹布局中的“Doxyfile.in”),这个代码有被find并被CMake的parsing命令代替的标记。 在我的顶级CMakeLists文件中 ,你会发现一个这样的CMake功夫,与cmake-doxygen一起做了几件奇特的事情。

构build项目

我通常会赞成以下几点:

 ├── 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目录树中的子目录。

对于testing,我倾向于让它们“接近”它们所testing的文件,如果你最终得到了/ src中的子目录,那么如果他们想要find给定的文件的testing代码,那么其他人就很容易遵循这个范例。


testing

其次我想使用谷歌C + +testing框架unit testing我的代码,因为它似乎相当容易使用。

Gtest的确使用简单,function相当全面。 它可以很容易地与gmock一起使用来扩展它的能力,但是我自己的经验与gmock不太有利。 我很愿意接受,这可能是由于我自己的缺点,但gmocktesting往往是更难以创build,更脆弱/难以维持。 gmock棺材里的一个大问题就是它对于智能指针来说确实不好用。

对于一个巨大的问题(这可能不属于SO),这是一个非常小的和主观的答案,

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

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

看到我在这里处理具体问题的答案 。

在编写testing时,这些通常是如何组织的? 我想每个类都有一个cpp文件(例如test_vector3.cpp),但都编译成一个二进制文件,以便它们都可以轻松地运行在一起。

好计划。


build造

我是CMake的粉丝,但是和你的testing相关的问题一样,SO可能不是就这样的主观问题征求意见的最好的地方。

CMakeLists.txt如何查看,以便它可以只build立图书馆或图书馆和testing?

 add_library(ProjectLibrary <All library sources and headers>) add_executable(ProjectTest <All test files>) target_link_libraries(ProjectTest ProjectLibrary) 

该库将显示为“ProjectLibrary”的目标,并将testing套件作为目标“ProjectTest”显示。 通过将库指定为testingexe的依赖项,构buildtestingexe将自动使库在过期时被重build。

另外我也看到了不less有一个build目录的项目。 构build是否发生在构build目录中,然后将二进制文件移出到bin目录中? 考试的二进制文件和图书馆会住在同一个地方吗?

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

可以指定二进制文件被移动或复制到另一个目录,或者它们是在另一个目录中默认创build的,但通常不需要。 CMake提供了全面的方法来安装您的项目,或者使其他CMake项目能够轻松地“find”您项目的相关文件。

关于CMake自己对查找和执行gtesttesting的支持,如果你把gtest作为你的项目的一部分,这在很大程度上是不合适的。 FindGtest模块确实被devise用于在你的项目之外单独构buildgtest的情况。

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

但是,由GTEST_ADD_TESTS提供的GTEST_ADD_TESTSmacros允许轻松添加gtest个案作为单独的ctest个案缺乏,因为除了TESTTEST_F以外,它不适用于gtest的macros。 使用TEST_PTYPED_TEST_P等进行的值或types参数化testingTEST_P不处理。

这个问题没有一个我知道的简单的解决scheme。 获取gtest列表的最可靠方法是使用标志--gtest_list_tests执行testingexe。 但是,这个只能在exebuild好之后才能完成,所以CMake不能利用这个。 这给你两个select, CMake必须尝试parsingC ++代码以推导出testing的名称(如果要考虑所有gtestmacros,注释掉的testing,禁用的testing,那么极端不平凡),或者手动添加testing用例CMakeLists.txt文件。

我也想用doxygen来logging我的代码。 是否有可能得到这个自动与cmake运行,使?

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

除了其他(优秀)的答案外,我将描述一个我用于比较大规模项目的结构。
我不打算讨论关于Doxygen的子问题,因为我只是重复其他答案中的内容。


合理

为了模块化和可维护性,项目组织为一组小单元。 为了清楚起见,我们将它们命名为UnitX,其中X = A,B,C …(但是它们可以有任何通用名称)。 目录结构然后被组织以反映这个select,如果需要的话可以将单元分组。

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

 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目录,因为在configuration这些单元时已经指定了这个目录。 此外, 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 bsplinesTests COMMAND bsplinesTests ) 

使用GoogleTest

对于Googletesting来说,最简单的方法是,如果源代码位于源代码目录的某个位置,则不必亲自添加。 我一直在使用这个项目自动下载它,我把它的使用封装在一个函数中,以确保它只下载一次,即使我们有几个testing目标。

这个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() 

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

 import_gtest() target_link_libraries(UnitDTests gtest_main gmock_main)