写在前面
这是本人在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的朋友,链接点此处:原文.
文中暴露了原本我项目存在的一些问题:
源码文件随意堆放
层次不清,不利于管理。
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
中将文档文件渲染展示出来。没有一个好的方法让其他开发者能够快速引入项目库的代码,需要其他开发者自己配置,增大学习交流和协同开发的成本。
手写测试
测试可读性差,不便于比对测试结果。
改造方案
针对上述问题,我对项目进行了相关的调整与改造:
源码结构调整
源码文件夹如下所示:
其中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
这样给予用户充分的选择权,可以直接引入original.h
项目主头文件免去引入其他任何头文件的麻烦,或者为了避免编译时间过长,引入如#include "vector.h"
这样单个头文件也能成为可能,以及以上两者取其中而引入某个模块的头文件#include "core.h"
.
部署在线文档
文档链接:文档-Original
在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
输出的项目文件夹如下:
将其同步到github仓库后,我们使用Vercel平台在线部署我们的文档:
"Framework Preset"的框架处选择“其他”,因为Doxygen生成的是静态网页,根目录选择docs/html
,因为项目主页文件index.html
在此文件夹下。点击部署按钮即可完成在线文档的部署。
Cmake配置构建结构调整
此处的调整为这次调整的核心内容,极大地方便了项目的构建和分发。在便于项目管理的同时,增加了库的构建和引入方法:
主要改动是:
配置主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
文件夹下的结构:
随着模块的增多,还会添加对于更多模块的测试,文件夹格式: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
下的结构:
# 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
断言宏用于断定测试代码是否会抛出异常(实际抛出通过测试,实际未抛出则测试失败)。
总结
通过给自己的项目的相关现代化改造,使得项目变得更好被维护。他人的学习和使用成本也会降低,自己也学会了很多,受益匪浅。
欢迎各位在评论区的讨论~