clang最小可编译的版本和标准是clang3.4.1+cpp11:

clang最小可编译的版本和标准是clang3.4.1+cpp11:

原问题发布在我项目的issue专栏中:
https://github.com/FrozenLemonTee/original/issues/9
具体细节请看原问题,此处不过多赘述。
大致情况就是我在开启cmake Release优化后,对我的基础库项目中的双向链表使用gtest进行单元测试时发生崩溃。而且错误仅在对双向链表的相等运算符进行单元测试时发生,该源文件中仅有这一项测试会崩溃,同时具有相似实现的单向链表也有此问题。
补充一些环境信息:
发生崩溃的现象本人已在以下环境复现:
这是本人在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_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在此文件夹下。点击部署按钮即可完成在线文档的部署。
此处的调整为这次调整的核心内容,极大地方便了项目的构建和分发。在便于项目管理的同时,增加了库的构建和引入方法:

主要改动是:
配置主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配置的维护和管理。
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断言宏用于断定测试代码是否会抛出异常(实际抛出通过测试,实际未抛出则测试失败)。
通过给自己的项目的相关现代化改造,使得项目变得更好被维护。他人的学习和使用成本也会降低,自己也学会了很多,受益匪浅。
欢迎各位在评论区的讨论~
已完成。
最终解决方案是,通过cloneable接口的clone方法,实现具体类对象的动态创建:
https://github.com/FrozenLemonTee/original/commit/fe14776ccc411790084dcd4ea1a002d3ee22eaa7
https://github.com/FrozenLemonTee/original/commit/cdd94d92c29c09bb58ca4d6f4b9eadb8272e7e27
可以具体说一下怎么做吗
相关测试的例子在项目的test/test2.cpp中,最后几行就是针对该问题的测试
项目地址:https://github.com/FrozenLemonTee/original
是一个仿STL的工具库项目。
问题源自于transform和transformStream两个类的设计上。transform是一个接口,通过继承这个类并且实现apply方法,可以直接以仿函数的方式调用这个类,将定义好的变换施加给项目中继承了iterable接口的可迭代对象中存放的所有元素。transformStream是一个中间的临时类,同时和transform一样也是可调用类,用于对若干个传入函数的transform所构成的复合调用链进行管理,在自己被调用时按照调用链顺序依次调用相关的transform以达到复合调用的目的。复合调用链用+操作符进行连接,通过重载+操作符自动生成transformStream,并将调用链中的transform加入生成的transformStream的管理。
在测试中发现,传入函数的复合调用链的每个transform派生类对象在被调用时,调用的是基类的apply方法,而基类定义的是空操作,也就导致整个调用链被调用时不施加任何变换。经过检查是transformStream的pushEnd方法存在问题:
template<typename TYPE>
void original::transformStream<TYPE>::pushEnd(const transform<TYPE>& t) {
this->stream.pushEnd(std::make_shared<std::decay_t<decltype(t)>>(t));
}
当以智能指针将传入的transform包装时,智能指针的参数无法获得传入的对象t正确的派生类型,导致对象t以基类的形式被存储。
我尝试了很多方案想要解决这个问题,一开始怀疑是自定义的容器chain的问题,于是换成std::vector,或者考虑以std::function的方式存储变换,但是都无效,想请教一下如何设计可以避免这个问题?