<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[现代C++项目开发初识——记项目Orignal大改造]]></title><description><![CDATA[<h2>写在前面</h2>
<hr />
<p dir="auto">这是本人在D2Learn的第一篇Blog，敬请指教~</p>
<p dir="auto">本文项目地址：<a href="https://github.com/FrozenLemonTee/original" rel="nofollow ugc">https://github.com/FrozenLemonTee/original</a></p>
<p dir="auto">欢迎提出宝贵意见，如issue和pull request~</p>
<hr />
<h1>项目简介</h1>
<p dir="auto">目前在写的<a href="https://github.com/FrozenLemonTee/original" rel="nofollow ugc">Original</a>是一个仿照C++ STL的基础工具库，是几年前在复习考研时的项目<a href="https://github.com/FrozenLemonTee/data_structure_in_408" rel="nofollow ugc">data_structure_in_408</a>的重制升级版。当时对于现代C++标准还知之甚少，甚至面向对象的内容掌握的也一般，所以造成的结果是用C++语言写出了C风格的代码：<br />
因此在Original中果断摈弃了纯C的代码，大量采用现代C++语言标准（如：命名空间、auto、constexpr、mutable、using别名、const成员方法、列表式构造初始化、条件if编译、左值引用等）、模板编程（泛化与特化、CRTP、嵌套模板等）、面向对象（特殊构造方法、接口复用、虚函数继承与重写、友元与封装设计、多态基类引用和指针等），以充分满足项目业务需求，同时保证项目代码有足够的可读性和可维护性。</p>
<h1>改造起因</h1>
<p dir="auto">以上是项目相对于老项目的更新与改造，然而最近也学习到了现代项目更多的维护与管理方法。这里特别感谢给我项目提出Issue的朋友，链接点此处：<a href="https://github.com/FrozenLemonTee/original/issues/1" rel="nofollow ugc">原文</a>.<br />
文中暴露了原本我项目存在的一些问题：</p>
<h4>源码文件随意堆放</h4>
<p dir="auto"><img src="/assets/uploads/files/1736242556785-cfa7ced4-8525-4b20-977f-a50874019325-image.png" alt="cfa7ced4-8525-4b20-977f-a50874019325-image.png" class=" img-fluid img-markdown" /><br />
层次不清，不利于管理。</p>
<h4>CMake配置文件过于简单</h4>
<pre><code class="language-cmake">cmake_minimum_required(VERSION 3.29)
project(original)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)

include_directories(${CMAKE_SOURCE_DIR})


add_executable(test1 test/test1.cpp original.h)

add_executable(test2 test/test2.cpp original.h)

add_executable(test3 test/test3.cpp original.h)
</code></pre>
<p dir="auto">此处的问题也是不方便管理项目，过于扁平化。</p>
<h4>文档及教程缺少展示</h4>
<p dir="auto">仅仅只是将<code>docs</code>文件夹置于项目仓库中，并没有在<code>README.md</code>中将文档文件渲染展示出来。没有一个好的方法让其他开发者能够快速引入项目库的代码，需要其他开发者自己配置，增大学习交流和协同开发的成本。</p>
<h4>手写测试</h4>
<p dir="auto"><img src="/assets/uploads/files/1736243344258-0b431ff3-46d5-42a1-ad53-d626eed51593-image.png" alt="0b431ff3-46d5-42a1-ad53-d626eed51593-image.png" class=" img-fluid img-markdown" /><br />
测试可读性差，不便于比对测试结果。</p>
<h1>改造方案</h1>
<p dir="auto">针对上述问题，我对项目进行了相关的调整与改造：</p>
<h4>源码结构调整</h4>
<p dir="auto">源码文件夹如下所示：<br />
<img src="/assets/uploads/files/1736244052805-5e5e14bb-6a21-46a0-a949-7761eb5254ae-image.png" alt="5e5e14bb-6a21-46a0-a949-7761eb5254ae-image.png" class=" img-fluid img-markdown" /><br />
其中<code>original.h</code>是项目主头文件，会将每个模块的主头文件包含在内：</p>
<pre><code class="language-c++">#ifndef ORIGINAL_H
#define ORIGINAL_H

#include "core/core.h"
// other modules etc...

#endif //ORIGINAL_H

</code></pre>
<p dir="auto">每个模块的主头文件则包含了除自己以外该模块文件夹下的所有源文件：</p>
<pre><code class="language-c++">#ifndef CORE_H
#define CORE_H

#include "algorithms.h"
#include "array.h"
#include "bitSet.h"
#include "blocksList.h"
#include "chain.h"
#include "cloneable.h"
#include "comparator.h"
#include "container.h"
#include "couple.h"
#include "deque.h"
#include "doubleDirectionIterator.h"
#include "error.h"
#include "filter.h"
#include "filterStream.h"
#include "forwardChain.h"
#include "iterable.h"
#include "iterationStream.h"
#include "iterator.h"
#include "maths.h"
#include "printable.h"
#include "prique.h"
#include "queue.h"
#include "randomAccessIterator.h"
#include "serial.h"
#include "stack.h"
#include "stepIterator.h"
#include "transform.h"
#include "transformStream.h"
#include "vector.h"
#include "wrapper.h"

#endif //CORE_H

</code></pre>
<p dir="auto"><img src="/assets/uploads/files/1736244335234-d481d88d-5e1d-475a-8679-e4cae2799013-image.png" alt="d481d88d-5e1d-475a-8679-e4cae2799013-image.png" class=" img-fluid img-markdown" /><br />
这样给予用户充分的选择权，可以直接引入<code>original.h</code>项目主头文件免去引入其他任何头文件的麻烦，或者为了避免编译时间过长，引入如<code>#include "vector.h"</code>这样单个头文件也能成为可能，以及以上两者取其中而引入某个模块的头文件<code>#include "core.h"</code>.</p>
<h4>部署在线文档</h4>
<p dir="auto">文档链接：<a href="https://documents-original.vercel.app/" rel="nofollow ugc">文档-Original</a><br />
<img src="/assets/uploads/files/1736243708595-8493e7e8-3eec-4eee-9acf-2895a8a8e888-image.png" alt="8493e7e8-3eec-4eee-9acf-2895a8a8e888-image.png" class=" img-fluid img-markdown" /><br />
在<code>README.md</code>中将其展示。</p>
<p dir="auto">首先编辑<code>Doxyfile</code>配置文件：</p>
<pre><code class="language-ini"># 项目名称
PROJECT_NAME = "ORIGINAL"

# 源目录
INPUT = src

# 输出目录
OUTPUT_DIRECTORY = ../original_docs/docs

# 文件类型
FILE_PATTERNS = *.h

# 允许递归扫描源文件夹
RECURSIVE = YES

# 启用UML类图生成
HAVE_DOT = YES
CLASS_DIAGRAMS = YES

# 生成PlantUML文件
GENERATE_UML = YES
UML_LOOK = YES
UML_LIMIT_NUM_CLASSES = 0  # 设置类图的类数量限制

# 使用PlantUML格式输出
UML_OUTPUT_FORMAT = plantuml
</code></pre>
<p dir="auto">其中项目名称指定生成的文档所显示的项目名称，源目录和文件类型指定为哪些代码生成文档，递归扫描配合项目的层级结构使用，以避免遗漏层级中的某些文件，输出目录的设置是将文档生成在项目本地文件夹的隔壁文件夹里，方便利用git工具进行版本管理和在线部署。</p>
<p dir="auto">在目录下输入以下命令即可生成文档静态网页文件：</p>
<pre><code class="language-bash">doxygen
</code></pre>
<p dir="auto">输出的项目文件夹如下：<br />
<img src="/assets/uploads/files/1736245409689-428aff30-9ed0-4946-a70b-9efad0f1cca8-image.png" alt="428aff30-9ed0-4946-a70b-9efad0f1cca8-image.png" class=" img-fluid img-markdown" /><br />
将其同步到github仓库后，我们使用<a href="https://vercel.com/" rel="nofollow ugc">Vercel</a>平台在线部署我们的文档：<br />
<img src="/assets/uploads/files/1736245671093-ec550656-c3a8-46f0-9da9-c4e20f9684bb-image.png" alt="ec550656-c3a8-46f0-9da9-c4e20f9684bb-image.png" class=" img-fluid img-markdown" /><br />
"Framework Preset"的框架处选择“其他”，因为Doxygen生成的是静态网页，根目录选择<code>docs/html</code>，因为项目主页文件<code>index.html</code>在此文件夹下。点击部署按钮即可完成在线文档的部署。</p>
<h4>Cmake配置构建结构调整</h4>
<p dir="auto">此处的调整为这次调整的核心内容，极大地方便了项目的构建和分发。在便于项目管理的同时，增加了库的构建和引入方法：<br />
<img src="/assets/uploads/files/1736246494757-qq%E6%88%AA%E5%9B%BE20250107184050.png" alt="QQ截图20250107184050.png" class=" img-fluid img-markdown" /></p>
<p dir="auto">主要改动是：<br />
配置主<code>CmakeLists.txt</code>：</p>
<pre><code class="language-cmake">cmake_minimum_required(VERSION 3.30)
project(original LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
set(CMAKE_BUILD_TYPE Debug)

include_directories(${CMAKE_SOURCE_DIR})
include_directories(${CMAKE_SOURCE_DIR}/src/core)

file(GLOB CORE_HEADERS "${CMAKE_SOURCE_DIR}/src/original.h")

add_library(original STATIC ${CORE_HEADERS} src/original.cpp)

install(TARGETS original DESTINATION lib)
install(FILES src/original.h DESTINATION include)
install(DIRECTORY src/core/
        DESTINATION include/core
        FILES_MATCHING PATTERN "*.h")

set(CMAKE_INSTALL_PREFIX ${CMAKE_SOURCE_DIR}/install)
include(CMakePackageConfigHelpers)
configure_package_config_file(
        "${CMAKE_CURRENT_SOURCE_DIR}/cmake/originalConfig.cmake.in"
        "${CMAKE_BINARY_DIR}/cmake/originalConfig.cmake"
        INSTALL_DESTINATION cmake/original
)

install(FILES "${CMAKE_BINARY_DIR}/cmake/originalConfig.cmake"
        DESTINATION cmake/original)

# test cases
add_subdirectory(test/legacy)
add_subdirectory(test/unit_test)

</code></pre>
<p dir="auto">其中<code>include_directories</code>相关命令让cmake能够找到项目源码所在的目录，使得接下来的命令操作成为可能。<code>file</code>命令在于打包项目主头文件，以便找到项目所有的头文件。<code>add_library</code>处创建一个可执行文件用于构建动态链接文件。<code>install</code>命令则指定安装时将所有的头文件都正确地被复制。</p>
<pre><code class="language-cmake">set(CMAKE_INSTALL_PREFIX ${CMAKE_SOURCE_DIR}/install)
include(CMakePackageConfigHelpers)
configure_package_config_file(
        "${CMAKE_CURRENT_SOURCE_DIR}/cmake/originalConfig.cmake.in"
        "${CMAKE_BINARY_DIR}/cmake/originalConfig.cmake"
        INSTALL_DESTINATION cmake/original
)

install(FILES "${CMAKE_BINARY_DIR}/cmake/originalConfig.cmake"
        DESTINATION cmake/original)
</code></pre>
<p dir="auto">以上的命令用于配置库在安装后的配置信息，使得cmake可以通过<code>find_package</code>命令找到original的库文件内容。<br />
项目文件夹下的<code>cmake/originalConfig.cmake.in</code>的内容如下：</p>
<pre><code class="language-cmake"># cmake/originalConfig.cmake.in

@PACKAGE_INIT@

set(ORIGINAL_INCLUDE_DIRS "${CMAKE_SOURCE_DIR}/install/include")
if(WIN32)
    set(ORIGINAL_LIBRARIES "${CMAKE_SOURCE_DIR}/install/lib/original.lib")
else()
    set(ORIGINAL_LIBRARIES "${CMAKE_SOURCE_DIR}/install/lib/liboriginal.a")
endif()

include_directories(${ORIGINAL_INCLUDE_DIRS})

if (ORIGINAL_LIBRARIES)
    link_libraries(${ORIGINAL_LIBRARIES})
endif()
</code></pre>
<p dir="auto">这个文件是cmake模版文件，在安装时会生成同名的<code>.cmake</code>后缀文件，此处配置安装路径的头文件（include）路径和链接文件路径（.a和.lib）。</p>
<pre><code class="language-cmake"># test cases
add_subdirectory(test/legacy)
add_subdirectory(test/unit_test)
</code></pre>
<p dir="auto">此处是添加子文件夹，用于引用这些目录下的<code>CmakeLists.txt</code>文件。通过拆分的方式避免所有内容都写在同一个配置文件中，有利于cmake配置的维护和管理。</p>
<h4>使用GTest+Cmake进行单元测试</h4>
<p dir="auto"><a href="https://github.com/google/googletest" rel="nofollow ugc">Google Test（GTest）</a>是谷歌开发的测试框架，这里我们利用这个框架来进行单元测试。<br />
在<code>test/unit_test</code>文件夹下的结构：<br />
<img src="/assets/uploads/files/1736248405168-a3be6864-fc43-4e39-9f91-873606450000-image.png" alt="a3be6864-fc43-4e39-9f91-873606450000-image.png" class=" img-fluid img-markdown" /><br />
随着模块的增多，还会添加对于更多模块的测试，文件夹格式：<code>test_&lt;module_name&gt;</code>.<br />
cmake配置文件：</p>
<pre><code class="language-cmake"># test/unit_test/CMakeLists.txt

include(FetchContent)

FetchContent_Declare(
        GTest
        GIT_REPOSITORY https://github.com/google/googletest.git
        GIT_TAG release-1.11.0
)

FetchContent_MakeAvailable(GTest)

add_subdirectory(test_core)
</code></pre>
<p dir="auto">此处通过cmake的<code>FetchContent</code>模块远程拉取GTest模块的内容。<br />
每个<code>add_subdirectory</code>命令用于引入每个测试模块文件夹。<br />
<code>test_core</code>下的结构：<br />
<img src="/assets/uploads/files/1736248841559-d87a6d16-c965-427a-a9d2-9d345bd1214f-image.png" alt="d87a6d16-c965-427a-a9d2-9d345bd1214f-image.png" class=" img-fluid img-markdown" /></p>
<pre><code class="language-cmake"># test/unit_test/test_core/CMakeLists.txt

file(GLOB CORE_TESTS "test_*.cpp")

add_executable(core_tests ${CORE_TESTS})

target_link_libraries(core_tests original)
target_link_libraries(core_tests GTest::gtest GTest::gmock GTest::gmock_main GTest::gtest_main)

add_test(NAME CoreTests COMMAND core_tests)
</code></pre>
<p dir="auto">此处配置文件首先收集目录下所有的测试文件（均以<code>.h</code>后缀结尾），统一通过一条<code>add_executable</code>添加为可执行文件，避免了逐一添加的麻烦。<code>target_link_libraries</code>链接了被测试的库（也就是我们的项目Original）和测试框架GTest，通过<code>add_test</code>将该测试模块下的所有测试文件统一注册。</p>
<p dir="auto">由于测试文件内容较多，此处选择其中一个文件进行展示：</p>
<pre><code class="language-c++">#include &lt;gtest/gtest.h&gt;
#include &lt;stdexcept&gt;
#include "error.h"

using namespace original;

TEST(ErrorTest, OutOfBoundErrorTest) {
    outOfBoundError e;
    EXPECT_STREQ(e.what(), "Out of the bound of the object.");
}

TEST(ErrorTest, ValueErrorTest) {
    valueError e;
    EXPECT_STREQ(e.what(), "Wrong value given.");
}

TEST(ErrorTest, NullPointerErrorTest) {
    nullPointerError e;
    EXPECT_STREQ(e.what(), "Attempting to access null pointer.");
}

TEST(ErrorTest, UnSupportedMethodErrorTest) {
    unSupportedMethodError e;
    EXPECT_STREQ(e.what(), "Unsupported Method for class.");
}

TEST(ErrorTest, NoElementErrorTest) {
    noElementError e;
    EXPECT_STREQ(e.what(), "No such element.");
}


void validCallback(int a, double b) {
    std::cout &lt;&lt; "a + b = " &lt;&lt; a + b &lt;&lt; std::endl;
}

int invalidReturnTypeCallback(int a, double b) {
    return a + static_cast&lt;int&gt;(b);
}


void invalidArgumentCallback(int a) {
    std::cout &lt;&lt; a &lt;&lt; std::endl;
}

class sampleClass{};

TEST(CallBackCheckerTest, ValidCallbackTest) {
    EXPECT_NO_THROW((original::callBackChecker::check&lt;decltype(validCallback), void, int, double&gt;()));
}

TEST(CallBackCheckerTest, InvalidCallbackTest) {
    EXPECT_THROW((original::callBackChecker::check&lt;decltype(invalidReturnTypeCallback), float, int, double&gt;()), CallbackReturnTypeError);
}

TEST(CallBackCheckerTest, InvalidArgumentCallbackTest1) {
    EXPECT_THROW((original::callBackChecker::check&lt;decltype(invalidArgumentCallback), void, int, int&gt;()), CallbackSignatureError);
}


TEST(CallBackCheckerTest, InvalidArgumentCallbackTest2) {
    EXPECT_THROW((original::callBackChecker::check&lt;decltype(invalidArgumentCallback), void, sampleClass&gt;()), CallbackSignatureError);
}

TEST(CallBackCheckerTest, EmptyCallbackTest) {
    EXPECT_NO_THROW((original::callBackChecker::check&lt;std::function&lt;void()&gt;, void&gt;()));
}
</code></pre>
<p dir="auto">每个<code>TEST</code>宏代码块就是一个测试单元，<code>EXPECT_STREQ</code>断言宏用于比较两个字符串是否相等，<code>EXPECT_NO_THROW</code>断言宏用于断定测试代码是否<strong>不会</strong>抛出异常（实际未抛出通过测试，实际抛出则测试失败），<code>EXPECT_THROW</code>断言宏用于断定测试代码是否<strong>会</strong>抛出异常（实际抛出通过测试，实际未抛出则测试失败）。</p>
<h1>总结</h1>
<p dir="auto">通过给自己的项目的相关现代化改造，使得项目变得更好被维护。他人的学习和使用成本也会降低，自己也学会了很多，受益匪浅。</p>
<p dir="auto">欢迎各位在评论区的讨论~</p>
]]></description><link>http://forum.d2learn.org/topic/61/现代c-项目开发初识-记项目orignal大改造</link><generator>RSS for Node</generator><lastBuildDate>Thu, 13 Aug 2026 15:50:10 GMT</lastBuildDate><atom:link href="http://forum.d2learn.org/topic/61.rss" rel="self" type="application/rss+xml"/><pubDate>Tue, 07 Jan 2025 11:39:53 GMT</pubDate><ttl>60</ttl><item><title><![CDATA[Reply to 现代C++项目开发初识——记项目Orignal大改造 on Tue, 07 Jan 2025 13:48:02 GMT]]></title><description><![CDATA[<p dir="auto">感觉关于项目相关的工程 (软件工程) 方面的东西是挺重要的</p>
<ul>
<li>目录结构</li>
<li>单元测试</li>
<li>文档</li>
<li>构建</li>
<li>...</li>
</ul>
<p dir="auto">了解这些后。要自己开发项目或要去使用/阅读一些其他项目, 就能很快上手并理解很多项目的结构以及他们为什么那么做</p>
<p dir="auto">(有时间感觉可以尝试把文章的一些内容 录成视频分享到视频平台, 应该会有不少人感兴趣)</p>
]]></description><link>http://forum.d2learn.org/post/223</link><guid isPermaLink="true">http://forum.d2learn.org/post/223</guid><dc:creator><![CDATA[sunrisepeak]]></dc:creator><pubDate>Tue, 07 Jan 2025 13:48:02 GMT</pubDate></item></channel></rss>