C++20的requires表达式是唯一静态、清晰、可诊断地约束模板参数具有某公有成员变量的标准方案;它通过requires(T t) { t.member; }验证语法与访问合法性,不构造对象,且仅适用于公有成员。

直接说结论:C++20 的 requires 表达式是目前唯一能**静态、清晰、可诊断**地约束“模板参数必须有某公共成员变量”的标准方案;C++17 及以前只能靠 std::void_t + 成员指针探测,但错误信息晦涩、写法易错。
用 requires(T t) { t.member; } 检查公有成员变量存在性
这是最常用、语义最直白的方式。它不构造对象,只验证表达式 t.member 在语法和访问权限上是否合法。
- 必须传入左值引用或值类型参数(如
T t或T& t),不能写requires { T::member; }—— 那是查静态成员 - 若
member是私有或受保护的,该检查失败(符合预期,因函数体里也访问不到) - 不关心类型,只管“能写出来”;要加类型约束需额外写
{ t.value } -> std::same_as<int>;</int> - 示例:
template<typename T> concept HasId = requires(T t) { t.id; }; template<HasId T> void log_id(const T& obj) { std::cout << "id = " << obj.id << '\n'; }
用 requires { typename T::member_type; } 检查嵌套类型别名
当你要约束的是 using value_type = int; 这类声明,而不是运行时可访问的变量,就该用 typename 形式 —— 它不触发访问检查,只看类型定义是否存在。
- 不能写成
typename T::member_type{},那会尝试默认构造,引入额外要求 - 错误提示更干净:编译器直接报
T does not declare 'value_type',而不是一长串 SFINAE 展开 - 适用于
std::vector、自定义容器等含value_type别名的类型 - 示例:
template<typename T> concept HasValueType = requires { typename T::value_type; }; template<HasValueType T> void process(const T& c) { using V = typename T::value_type; }
C++17 兼容方案:std::void_t<decltype></decltype> 探测成员变量
没有 C++20 时,这是最靠谱的替代:它基于 SFINAE 原理,在模板推导阶段就拦截,比 static_assert 或硬写 T::member 提前得多。
立即学习“C++免费学习笔记(深入)”;
- 必须用
std::void_t包裹decltype(&T::member),否则编译器在偏特化前就报错,SFINAE 失效 - 要搭配
std::is_member_object_pointer过滤掉函数指针,避免误判 - 宏可参数化成员名,但无法像 Concepts 那样自然组合多个条件
- 典型写法:
template<typename T, typename = void> struct HasId : std::false_type {}; template<typename T> struct HasId<T, std::void_t<decltype(&T::id)>> : std::is_member_object_pointer<decltype(&T::id)> {};
为什么 static_assert 和直接访问都不可行
这两类写法看似简单,实则完全绕不开编译期约束的本质问题。
-
static_assert(t.id, ...)放在函数体内?不行——t.id是运行时表达式,未定义时直接硬报错,不进函数体,更谈不上 SFINAE 回退 - 在模板参数里写
typename T::id?也不行——它属于“非依赖名称”,编译器在解析模板定义时就尝试求值,错误堆栈指向内部实现而非调用点 - 想用
std::enable_if?必须放在模板参数或返回类型中,放函数体里等于没写 - 真正关键点在于:约束必须发生在**模板参数推导/匹配阶段**,而不是实例化后;只有 Concepts 和 SFINAE trait 能做到这点
最容易被忽略的是访问权限与语义差异:requires(T t) { t.id; } 和 decltype(&T::id) 看似都在查成员,但前者要求可访问+可取值,后者只要求声明存在(哪怕私有)。选哪种,取决于你函数体里到底怎么用那个成员。


















