跳转至内容
  • 版块
  • 最新
  • 标签
  • 热门
  • Online Tools
  • 用户
  • 群组
折叠
品牌标识

D2Learn Forums

FrozenLemonTeeF

FrozenLemonTee

@FrozenLemonTee
关于
帖子
5
主题
2
群组
0
粉丝
0
关注
0

帖子

最新 最佳 有争议的

  • 现代C++项目开发初识——记项目Orignal大改造
    FrozenLemonTeeF FrozenLemonTee

    写在前面


    这是本人在D2Learn的第一篇Blog,敬请指教~

    本文项目地址:https://github.com/FrozenLemonTee/original

    欢迎提出宝贵意见,如issue和pull request~


    项目简介

    目前在写的Original是一个仿照C++ STL的基础工具库,是几年前在复习考研时的项目data_structure_in_408的重制升级版。当时对于现代C++标准还知之甚少,甚至面向对象的内容掌握的也一般,所以造成的结果是用C++语言写出了C风格的代码:
    因此在Original中果断摈弃了纯C的代码,大量采用现代C++语言标准(如:命名空间、auto、constexpr、mutable、using别名、const成员方法、列表式构造初始化、条件if编译、左值引用等)、模板编程(泛化与特化、CRTP、嵌套模板等)、面向对象(特殊构造方法、接口复用、虚函数继承与重写、友元与封装设计、多态基类引用和指针等),以充分满足项目业务需求,同时保证项目代码有足够的可读性和可维护性。

    改造起因

    以上是项目相对于老项目的更新与改造,然而最近也学习到了现代项目更多的维护与管理方法。这里特别感谢给我项目提出Issue的朋友,链接点此处:原文.
    文中暴露了原本我项目存在的一些问题:

    源码文件随意堆放

    cfa7ced4-8525-4b20-977f-a50874019325-image.png
    层次不清,不利于管理。

    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)
    

    此处的问题也是不方便管理项目,过于扁平化。

    文档及教程缺少展示

    仅仅只是将docs文件夹置于项目仓库中,并没有在README.md中将文档文件渲染展示出来。没有一个好的方法让其他开发者能够快速引入项目库的代码,需要其他开发者自己配置,增大学习交流和协同开发的成本。

    手写测试

    0b431ff3-46d5-42a1-ad53-d626eed51593-image.png
    测试可读性差,不便于比对测试结果。

    改造方案

    针对上述问题,我对项目进行了相关的调整与改造:

    源码结构调整

    源码文件夹如下所示:
    5e5e14bb-6a21-46a0-a949-7761eb5254ae-image.png
    其中original.h是项目主头文件,会将每个模块的主头文件包含在内:

    #ifndef ORIGINAL_H
    #define ORIGINAL_H
    
    #include "core/core.h"
    // other modules etc...
    
    #endif //ORIGINAL_H
    
    

    每个模块的主头文件则包含了除自己以外该模块文件夹下的所有源文件:

    #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
    
    

    d481d88d-5e1d-475a-8679-e4cae2799013-image.png
    这样给予用户充分的选择权,可以直接引入original.h项目主头文件免去引入其他任何头文件的麻烦,或者为了避免编译时间过长,引入如#include "vector.h"这样单个头文件也能成为可能,以及以上两者取其中而引入某个模块的头文件#include "core.h".

    部署在线文档

    文档链接:文档-Original
    8493e7e8-3eec-4eee-9acf-2895a8a8e888-image.png
    在README.md中将其展示。

    首先编辑Doxyfile配置文件:

    # 项目名称
    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
    

    其中项目名称指定生成的文档所显示的项目名称,源目录和文件类型指定为哪些代码生成文档,递归扫描配合项目的层级结构使用,以避免遗漏层级中的某些文件,输出目录的设置是将文档生成在项目本地文件夹的隔壁文件夹里,方便利用git工具进行版本管理和在线部署。

    在目录下输入以下命令即可生成文档静态网页文件:

    doxygen
    

    输出的项目文件夹如下:
    428aff30-9ed0-4946-a70b-9efad0f1cca8-image.png
    将其同步到github仓库后,我们使用Vercel平台在线部署我们的文档:
    ec550656-c3a8-46f0-9da9-c4e20f9684bb-image.png
    "Framework Preset"的框架处选择“其他”,因为Doxygen生成的是静态网页,根目录选择docs/html,因为项目主页文件index.html在此文件夹下。点击部署按钮即可完成在线文档的部署。

    Cmake配置构建结构调整

    此处的调整为这次调整的核心内容,极大地方便了项目的构建和分发。在便于项目管理的同时,增加了库的构建和引入方法:
    QQ截图20250107184050.png

    主要改动是:
    配置主CmakeLists.txt:

    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)
    
    

    其中include_directories相关命令让cmake能够找到项目源码所在的目录,使得接下来的命令操作成为可能。file命令在于打包项目主头文件,以便找到项目所有的头文件。add_library处创建一个可执行文件用于构建动态链接文件。install命令则指定安装时将所有的头文件都正确地被复制。

    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)
    

    以上的命令用于配置库在安装后的配置信息,使得cmake可以通过find_package命令找到original的库文件内容。
    项目文件夹下的cmake/originalConfig.cmake.in的内容如下:

    # 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()
    

    这个文件是cmake模版文件,在安装时会生成同名的.cmake后缀文件,此处配置安装路径的头文件(include)路径和链接文件路径(.a和.lib)。

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

    此处是添加子文件夹,用于引用这些目录下的CmakeLists.txt文件。通过拆分的方式避免所有内容都写在同一个配置文件中,有利于cmake配置的维护和管理。

    使用GTest+Cmake进行单元测试

    Google Test(GTest)是谷歌开发的测试框架,这里我们利用这个框架来进行单元测试。
    在test/unit_test文件夹下的结构:
    a3be6864-fc43-4e39-9f91-873606450000-image.png
    随着模块的增多,还会添加对于更多模块的测试,文件夹格式:test_<module_name>.
    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)
    

    此处通过cmake的FetchContent模块远程拉取GTest模块的内容。
    每个add_subdirectory命令用于引入每个测试模块文件夹。
    test_core下的结构:
    d87a6d16-c965-427a-a9d2-9d345bd1214f-image.png

    # 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)
    

    此处配置文件首先收集目录下所有的测试文件(均以.h后缀结尾),统一通过一条add_executable添加为可执行文件,避免了逐一添加的麻烦。target_link_libraries链接了被测试的库(也就是我们的项目Original)和测试框架GTest,通过add_test将该测试模块下的所有测试文件统一注册。

    由于测试文件内容较多,此处选择其中一个文件进行展示:

    #include <gtest/gtest.h>
    #include <stdexcept>
    #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 << "a + b = " << a + b << std::endl;
    }
    
    int invalidReturnTypeCallback(int a, double b) {
        return a + static_cast<int>(b);
    }
    
    
    void invalidArgumentCallback(int a) {
        std::cout << a << std::endl;
    }
    
    class sampleClass{};
    
    TEST(CallBackCheckerTest, ValidCallbackTest) {
        EXPECT_NO_THROW((original::callBackChecker::check<decltype(validCallback), void, int, double>()));
    }
    
    TEST(CallBackCheckerTest, InvalidCallbackTest) {
        EXPECT_THROW((original::callBackChecker::check<decltype(invalidReturnTypeCallback), float, int, double>()), CallbackReturnTypeError);
    }
    
    TEST(CallBackCheckerTest, InvalidArgumentCallbackTest1) {
        EXPECT_THROW((original::callBackChecker::check<decltype(invalidArgumentCallback), void, int, int>()), CallbackSignatureError);
    }
    
    
    TEST(CallBackCheckerTest, InvalidArgumentCallbackTest2) {
        EXPECT_THROW((original::callBackChecker::check<decltype(invalidArgumentCallback), void, sampleClass>()), CallbackSignatureError);
    }
    
    TEST(CallBackCheckerTest, EmptyCallbackTest) {
        EXPECT_NO_THROW((original::callBackChecker::check<std::function<void()>, void>()));
    }
    

    每个TEST宏代码块就是一个测试单元,EXPECT_STREQ断言宏用于比较两个字符串是否相等,EXPECT_NO_THROW断言宏用于断定测试代码是否不会抛出异常(实际未抛出通过测试,实际抛出则测试失败),EXPECT_THROW断言宏用于断定测试代码是否会抛出异常(实际抛出通过测试,实际未抛出则测试失败)。

    总结

    通过给自己的项目的相关现代化改造,使得项目变得更好被维护。他人的学习和使用成本也会降低,自己也学会了很多,受益匪浅。

    欢迎各位在评论区的讨论~


  • 项目中智能指针多态性丢失的问题
    FrozenLemonTeeF FrozenLemonTee

    已完成。
    最终解决方案是,通过cloneable接口的clone方法,实现具体类对象的动态创建:
    https://github.com/FrozenLemonTee/original/commit/fe14776ccc411790084dcd4ea1a002d3ee22eaa7
    https://github.com/FrozenLemonTee/original/commit/cdd94d92c29c09bb58ca4d6f4b9eadb8272e7e27

  • 登录

  • 没有帐号? 注册

  • 登录或注册以进行搜索。
d2learn forums Powered by NodeBB
  • 第一个帖子
    最后一个帖子
0
  • 版块
  • 最新
  • 标签
  • 热门
  • Online Tools
  • 用户
  • 群组