C++编译期间验证单个对象许可被释放、验证数组许可被释放和验证函数对象能否被指定类型参数调用
目录
1.核心原理
1.1.SFINAE
在 C++ 中,SFINAE 是 Substitution Failure Is Not An Error(替换失败不是错误)的缩写,是模板元编程中一项核心特性。它的核心思想是:当模板参数替换过程中出现无效代码时,编译器不会将其视为错误,而是忽略该模板特化 / 重载版本,继续寻找其他合法的候选版本。
模板在实例化时,编译器会尝试将实际参数 “替换” 进模板参数中。如果替换后出现语法或语义错误(如访问不存在的成员、调用不匹配的函数等),SFINAE 确保编译器不会直接报错,而是跳过这个无效的模板版本,选择其他可行的版本。
只有当所有模板版本都替换失败时,编译器才会报错。
1.2.SFINAE 的核心工具
SFINAE 通常结合以下工具实现更灵活的类型检查:
1.2.1.decltype 与逗号表达式
decltype(expression) 用于推导表达式的类型,若表达式无效则导致替换失败。逗号表达式 (expr1, expr2) 的结果为 expr2 的类型,可用于 “先检查 expr1 合法性,再返回 expr2 类型”。
示例:假设我们要实现两个函数模板,分别处理 “有 size() 成员的类型” 和 “普通类型”:
#include
#include
// 版本1:处理有 size() 成员的类型(通过 SFINAE 启用)
template
auto print_size(T& t) -> decltype(t.size(), void()) { // 若 t.size() 合法,则此版本有效
std::cout << "size: " << t.size() << std::endl;
}
// 版本2:处理无 size() 成员的类型(默认版本)
template
void print_size(T& t) {
std::cout << "no size() member" << std::endl;
}
int main() {
std::vector vec(5);
int num = 10;
print_size(vec); // 调用版本1:vec 有 size(),替换成功
print_size(num); // 调用版本2:num 无 size(),版本1替换失败,选择版本2
return 0;
}
decltype(t.size(), void()) 表示:
- 先检查
t.size()是否合法(若不合法,替换失败); - 若合法,返回
void类型(与函数返回值匹配)。
1.2.2.void_t(C++17 引入)
void_t 是一个辅助模板,定义为:
template
using void_t = void; // 将任意类型列表转换为 void
它的作用是将 “表达式合法性检查” 转化为模板参数的替换问题。若 void_t<...> 中的表达式无效,则模板特化被丢弃。
示例:检查类型是否有 value 成员
#include
// 基础模板:默认无 value 成员
template
struct has_value : std::false_type {};
// 特化模板:若 T::value 存在,则匹配此版本
template
struct has_value> : std::true_type {};
// 测试
struct A { static const int value = 10; };
struct B {};
static_assert(has_value::value, "A 有 value 成员"); // 成功
static_assert(!has_value::value, "B 无 value 成员"); // 成功
- 对
A而言,decltype(A::value)合法,void_t<...>为void,特化模板被选中(true_type)。 - 对
B而言,decltype(B::value)无效,特化模板替换失败,使用基础模板(false_type)。
推荐阅读:
1.2.3.模板参数的 “替换约束”
通过在模板参数中添加约束条件(如检查类型是否派生自某个基类、是否满足特定接口),利用 SFINAE 过滤无效版本。
示例:检查类型是否派生自 Base
struct Base {};
struct Derived : Base {};
struct Other {};
// 仅当 T 派生自 Base 时,此模板才有效
template >>
void func(T) {
std::cout << "T 是 Base 的派生类" << std::endl;
}
// 其他类型的重载
template >>
void func(T) {
std::cout << "T 不是 Base 的派生类" << std::endl;
}
int main() {
func(Derived()); // 调用第一个版本(Derived 派生自 Base)
func(Other()); // 调用第二个版本(Other 不派生自 Base)
return 0;
}
这里使用 std::enable_if_t(基于 SFINAE 实现),当条件为 true 时,enable_if_t 为 void(匹配模板参数),否则替换失败。
1.3.SFINAE 的典型应用场景
1.类型萃取(Type Traits)
标准库中的 std::is_pointer、std::has_virtual_destructor 等类型萃取工具,本质是通过 SFINAE 检查类型的特性。
2.函数重载决议
根据类型的特性(如是否有某个成员、是否为算术类型)自动选择合适的函数版本,如本文开头的 print_size 例子。
3.约束模板参数
在智能指针(如 shared_ptr)、容器等模板中,通过 SFINAE 确保传入的参数满足特定条件(如本文之前提到的 _Can_scalar_delete 检查指针能否被 delete 释放)。
4.模拟 “概念”(Concepts)
C++20 之前,SFINAE 是实现 “类型约束” 的主要方式(C++20 引入的 Concepts 是更直观的替代方案,但底层仍依赖 SFINAE 的思想)。
2.编译期间验证单个对象可以被释放
先上代码:
// 基础模板:默认不可用 scalar delete,继承 false_type
template
struct _Can_scalar_delete : false_type {};
// 特化模板:若 delete _Yty* 合法,则继承 true_type(排除 void 类型)
template
struct _Can_scalar_delete<_Yty, void_t())>>
: bool_constant> {};
不明白std::declval的可参考:
用于判断:对于类型 _Yty,delete _Yty* 操作是否合法(即能否用 scalar delete 释放单个对象)。
- 基础模板:默认情况下,假设
delete _Yty*不合法,继承std::false_type(值为false)。 - 特化模板:
- 第二个模板参数是
void_t<decltype(...)>,其中decltype(delete _STD declval<_Yty*>())用于检查delete (Yty*)表达式是否合法(能否通过编译)。 - 若表达式合法(即
_Yty*可以被delete释放),则特化模板被选中,且继承bool_constant<!is_void_v<_Yty>>—— 排除_Yty = void(因为delete void*是 C++ 未定义行为,不允许)。
- 第二个模板参数是
示例:
// int* 可以被 delete 释放,且 _Yty 不是 void → 结果为 true
static_assert(_Can_scalar_delete::value, "int* 可被 scalar delete");
// void* 不能被 delete 释放 → 结果为 false
static_assert(!_Can_scalar_delete::value, "void* 不可被 scalar delete");
// 数组类型的指针(如 int[])用 scalar delete 不合法(应使用 delete[])→ 结果为 false
static_assert(!_Can_scalar_delete::value, "数组指针不可用 scalar delete");
3.编译期间验证数组可以被释放
先上代码:
// 基础模板:默认不可用 array delete,继承 false_type
template
struct _Can_array_delete : false_type {};
// 特化模板:若 delete[] _Yty* 合法,则继承 true_type
template
struct _Can_array_delete<_Yty, void_t())>>
: true_type {};
用于判断:对于类型 _Yty,delete[] _Yty* 操作是否合法(即能否用 array delete 释放数组对象)。
- 基础模板:默认假设
delete[] _Yty*不合法,继承std::false_type。 - 特化模板:若
delete[] (Yty*)表达式合法(如数组指针int*可用delete[]释放),则特化模板被选中,继承std::true_type(值为true)。
示例:
// int* 作为数组指针(new int[10])可用 delete[] 释放 → 结果为 true
static_assert(_Can_array_delete::value, "int* 数组可被 array delete");
// 不完全类型(如前向声明的类)的数组指针可能无法 delete[] → 结果为 false
struct Incomplete;
static_assert(!_Can_array_delete::value, "不完全类型数组不可用 array delete");
4.验证函数对象能否被指定类型参数调用
先上代码:
// 基础模板:默认不可调用,继承 false_type
template
struct _Can_call_function_object : false_type {};
// 特化模板:若 _Fx 可被 _Arg 类型参数调用,则继承 true_type
template
struct _Can_call_function_object<_Fx, _Arg, void_t()(_STD declval<_Arg>()))>>
: true_type {};
用于判断:函数对象 _Fx 能否被调用,且参数类型为 _Arg(例如,自定义删除器能否接收资源指针作为参数)。
- 基础模板:默认假设
_Fx不能被_Arg类型参数调用,继承std::false_type。 - 特化模板:若表达式
_Fx()(_Arg)合法(即函数对象_Fx可以接收_Arg类型的参数并调用),则特化模板被选中,继承std::true_type。
示例:
// 自定义删除器:接收 int* 参数
auto int_deleter = [](int* p) { delete p; };
// 检查:int_deleter 能否被 int* 调用 → 结果为 true
static_assert(_Can_call_function_object::value, "int_deleter 可调用");
// 字符串删除器:接收 const char* 参数
struct StrDeleter { void operator()(const char* p) { delete[] p; } };
// 检查:StrDeleter 能否被 int* 调用 → 结果为 false(参数类型不匹配)
static_assert(!_Can_call_function_object::value, "StrDeleter 不可接收 int*");
5.实际用途
这些结构体在 shared_ptr 的构造函数中用于编译期验证,确保传入的指针或删除器符合要求:
1.在 shared_ptr 用原始指针构造时(如 shared_ptr<int>(new int)),通过 _Can_scalar_delete 确保 int* 可被 delete 释放,否则编译报错。
2.在 shared_ptr 管理数组时(C++17 及以上),通过 _Can_array_delete 确保数组指针可被 delete[] 释放。
template , _Can_array_delete<_Ux>, _Can_scalar_delete<_Ux>>,
_SP_convertible<_Ux, _Ty>>,
int> = 0>
explicit shared_ptr(_Ux* _Px) { // construct shared_ptr object that owns _Px
if constexpr (is_array_v<_Ty>) {
_Setpd(_Px, default_delete<_Ux[]>{});
} else {
_Temporary_owner<_Ux> _Owner(_Px);
_Set_ptr_rep_and_enable_shared(_Owner._Ptr, new _Ref_count<_Ux>(_Owner._Ptr));
_Owner._Ptr = nullptr;
}
}
3.在 shared_ptr 传入自定义删除器时(如 shared_ptr<FILE>(fp, fclose)),通过 _Can_call_function_object 确保删除器可接收 FILE* 类型参数,否则编译报错。
template , _Can_call_function_object<_Dx&, _Ux*&>,
_SP_convertible<_Ux, _Ty>>,
int> = 0>
shared_ptr(_Ux* _Px, _Dx _Dt) { // construct with _Px, deleter
_Setpd(_Px, _STD move(_Dt));
}
template , _Can_call_function_object<_Dx&, _Ux*&>,
_SP_convertible<_Ux, _Ty>>,
int> = 0>
shared_ptr(_Ux* _Px, _Dx _Dt, _Alloc _Ax) { // construct with _Px, deleter, allocator
_Setpda(_Px, _STD move(_Dt), _Ax);
}
template , _Can_call_function_object<_Dx&, nullptr_t&>>, int> = 0>
shared_ptr(nullptr_t, _Dx _Dt) { // construct with nullptr, deleter
_Setpd(nullptr, _STD move(_Dt));
}
6.总结
这三个模板结构体是 SFINAE 技术的典型应用,用于在编译期检查特定操作(delete、delete[]、函数调用)的合法性:
_Can_scalar_delete:验证delete _Yty*是否合法(单个对象释放)。_Can_array_delete:验证delete[] _Yty*是否合法(数组释放)。_Can_call_function_object:验证函数对象能否被指定类型参数调用(如删除器兼容性)。
它们在智能指针等模板库中用于增强类型安全,将运行时错误提前到编译期暴露。

浙公网安备 33010602011771号