如何确保调用sort的类满足C++三/零/五法则?
此背景仅用于说明问题的由来,您可直接跳至「问题」部分。
近期我们公司遇到了一些无法复现的崩溃问题。经调查发现,我们的vector实现默认使用qsort作为排序方法。崩溃的原因是qsort对非平凡可移动类会产生未定义行为。
我们修改了代码:对平凡可移动类保留qsort,对可移动构造且可移动赋值的类使用std::sort,其余类则不允许排序。但随后又出现了新的崩溃,因为部分类虽满足可移动构造和可移动赋值,却未遵循三法则。之前能正常运行是因为仅进行内存移动,但现在自动生成的移动构造函数被调用后就引发了问题。
以下是一段示例代码:仅做内存移动时运行正常,但调用移动构造函数会导致双重释放:
struct MyStruct { int* myPointer; MyStruct() { myPointer = new int; } ~MyStruct() { delete myPointer; } };
这个类的写法存在问题,这是根源所在,但由于我们要修改原有行为,且代码库包含数千个文件,无法逐一检查所有sort调用。由此引出我们的问题。
如何检查调用sort的类是否遵循三法则(或零法则、五法则),以确保其能正常工作?
我们曾尝试检查类是否可移动构造和可移动赋值,但这并不足够。例如上述示例结构体中,以下静态断言会通过:
static_assert(std::is_move_constructible<MyStruct>::value); static_assert(std::is_move_assignable<MyStruct>::value);
自定义析构函数会阻止隐式生成移动构造函数,但复制构造函数仍会被生成。而若复制构造函数已定义,该类就会被视为可移动构造。
解决方案
1. 静态断言组合检查
要判断类是否符合三/零/五法则,核心逻辑是:要么所有特殊成员函数都由编译器隐式生成(零法则),要么自定义了析构函数的同时,显式定义/删除了所有复制、移动相关操作(三/五法则)。可以通过以下模板函数实现编译期检查:
零法则合规性检查
template <typename T> constexpr bool follows_zero_rule() { return std::is_trivially_destructible_v<T> && std::is_trivially_copy_constructible_v<T> && std::is_trivially_copy_assignable_v<T> && std::is_trivially_move_constructible_v<T> && std::is_trivially_move_assignable_v<T>; }
三/五法则合规性检查
template <typename T> constexpr bool follows_three_five_rule() { // 自定义析构函数的类,必须保证复制/移动操作的一致性(要么都能调用,要么都被删除) bool has_custom_dtor = !std::is_trivially_destructible_v<T>; bool copy_ops_consistent = std::is_copy_constructible_v<T> == std::is_copy_assignable_v<T>; bool move_ops_consistent = std::is_move_constructible_v<T> == std::is_move_assignable_v<T>; return follows_zero_rule<T>() || (has_custom_dtor && copy_ops_consistent && move_ops_consistent); }
将检查嵌入sort调用逻辑:
template <typename T> void my_sort(std::vector<T>& vec) { static_assert(follows_three_five_rule<T>(), "Type T must follow zero/three/five rule to be sorted"); if (std::is_trivially_move_constructible_v<T> && std::is_trivially_move_assignable_v<T>) { qsort(vec.data(), vec.size(), sizeof(T), compare<T>); } else { std::sort(vec.begin(), vec.end()); } }
2. 编译与静态分析工具
- Clang-Tidy:启用
cppcoreguidelines-special-member-functions检查器,自动扫描违反三/五法则的类并发出警告,可集成到构建流程批量处理代码库。 - 编译器警告:GCC 10+或Clang开启
-Wdeprecated-copy参数,当类自定义析构但未显式处理复制操作时,编译器会触发警告。 - 静态分析工具:使用Coverity、Cppcheck等工具深度扫描代码,识别浅拷贝、双重释放等潜在问题,弥补静态断言的覆盖盲区。
3. 运行时辅助检测
在测试环境中启用地址 sanitizer(ASAN),编译时添加参数:
g++ -fsanitize=address -O0 your_code.cpp -o your_program
ASAN会在运行时实时检测双重释放、野指针等问题,帮助快速定位违反规则的类实例。
内容的提问来源于stack exchange,提问作者Nathan Loudjani

