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