C++20 中应优先使用 requires 表达式或自定义 concept 替代 SFINAE:通过 template<T> requires requires(T t) { t.serialize(); } 直接约束成员可调用,语义清晰、编译错误友好;自定义 concept(如 concept Serializable = requires(T t) { t.serialize(); };)提升复用性与可读性;注意 requires 仅在模板参数推导时生效,不阻止显式实参的强制实例化。

直接用 requires 表达式或自定义 concept,比 SFINAE 清晰、安全、易调试。C++20 起没必要再写一长串 std::enable_if + decltype(std::declval<t>().member)</t> 来检测成员存在。
如何用 requires 检测成员函数或成员变量
在模板参数后加 requires 子句,直接描述“类型必须能调用 .serialize()”这类语义要求:
- 错误写法(SFINAE 风格,冗长且难读):
template<typename t typename="std::void_t<decltype(std::declval<T">().serialize())>></typename> - 正确写法(C++20):
template<typename t> requires requires(T t) { t.serialize(); }</typename> -
requires(T t) { t.serialize(); }是一个“需求表达式”,编译器会尝试构造临时对象t并调用.serialize();失败则整个模板不参与重载 - 支持链式调用:
{ t.begin(); t.end(); *t.begin(); }可一次性验证迭代器接口 - 注意:
t是虚构对象,不实际构造,仅用于语法检查
为什么 std::void_t + decltype 的 SFINAE 写法容易出错
这种模式依赖“替换失败不报错”的机制,但极易因语法细节翻车:
- 漏写
typename:比如std::enable_if<...>::type</...>在 C++11/14 中必须加typename,否则报error: need ‘typename’ before ‘std::enable_if<...>::type’</...> - 误把
requires当成运行时条件:它纯属编译期检查,不能和if constexpr混用逻辑 - 嵌套过深导致错误信息指向
__detail::_Iter_cat这类内部符号,而非你写的模板调用点 - 无法区分“成员不存在”和“访问权限不足”——两者都触发 SFINAE 移除,但语义完全不同
自定义 concept 封装常见成员约束
把重复的 requires 逻辑抽成命名概念,复用性高、意图明确:
立即学习“C++免费学习笔记(深入)”;
template<typename T>
concept Serializable = requires(T t) {
t.serialize();
};
template<Serializable T>
void save_to_file(const T& obj) {
obj.serialize(); // 编译器已保证 .serialize() 存在且可调用
}
-
Serializable不是类型 trait(如std::is_copy_constructible_v),它要求类型提供可调用接口,而不仅是“有这个符号” - 不能只写
requires { T::value; }—— 这检测的是静态成员,不是实例成员;实例成员必须通过T t声明后调用 - 若需同时检查多个成员,合并到一个
requires块里,避免多个 concept 嵌套导致约束关系模糊
真正容易被忽略的是:Concepts 约束只作用于模板参数推导阶段,对显式指定的模板实参(如 func<int>()</int>)仍会强制实例化,此时 requires 失效——它不替代 static_assert,也不拦截函数体内的非法操作。


















