C++ Concepts 教程:从类型萃取迁移到 Concepts
本文按用途和分类介绍 C++20 Concepts。每个用法都给出旧写法和新写法对比:
- 旧写法:类型萃取、
std::enable_if、std::void_t、static_assert、SFINAE。
- 新写法:
concept、requires、标准库 concepts。
示例默认使用 C++20。
1 2 3 4 5 6 7
| #include <concepts> #include <type_traits> #include <utility> #include <vector> #include <string> #include <iterator> #include <ranges>
|
1. Concepts 解决什么问题
模板代码通常需要限制类型能力。例如:
- 只允许整数类型。
- 只允许能比较相等的类型。
- 只允许能调用
begin() / end() 的范围类型。
- 根据类型能力选择不同重载。
C++20 之前常用类型萃取和 SFINAE 实现这些约束,但代码可读性差,错误信息也不直观。Concepts 把“类型必须满足什么条件”变成显式语法。
2. 基础语法
2.1 定义一个 concept
旧写法:用类型萃取定义布尔值。
1 2 3 4 5
| template <class T> struct is_integer_like : std::bool_constant<std::is_integral_v<T>> {};
template <class T> inline constexpr bool is_integer_like_v = is_integer_like<T>::value;
|
新写法:用 concept 直接表达约束。
1 2
| template <class T> concept IntegerLike = std::is_integral_v<T>;
|
使用:
1 2 3 4
| template <IntegerLike T> T add_one(T value) { return value + 1; }
|
对比:
- 类型萃取表达的是“一个布尔判断”。
- concept 表达的是“模板参数必须满足的约束”。
- concept 可以直接出现在模板参数列表、
requires 子句和函数声明中。
2.2 三种常见写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| template <std::integral T> T f1(T value) { return value; }
template <class T> requires std::integral<T> T f2(T value) { return value; }
template <class T> T f3(T value) requires std::integral<T> { return value; }
|
建议:
- 简单单参数约束用
template <std::integral T>。
- 多个条件组合用
requires 子句。
- 约束成员函数、构造函数、运算符时常用尾置
requires。
3. 类型类别约束
这一组用于限制类型属于某个大类,例如整数、浮点、枚举、指针、类类型等。
3.1 整数类型
旧写法:std::enable_if_t<std::is_integral_v<T>>。
1 2 3 4 5 6 7
| template < class T, std::enable_if_t<std::is_integral_v<T>, int> = 0 > T twice(T value) { return value * 2; }
|
新写法:std::integral。
1 2 3 4
| template <std::integral T> T twice(T value) { return value * 2; }
|
对比:
- 旧写法把约束塞进模板参数列表,阅读时需要解析 SFINAE 技巧。
- 新写法直接说明
T 必须是整数类型。
3.2 浮点类型
旧写法:
1 2 3 4 5 6 7
| template < class T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0 > T half(T value) { return value / static_cast<T>(2); }
|
新写法:
1 2 3 4
| template <std::floating_point T> T half(T value) { return value / static_cast<T>(2); }
|
对比:
std::floating_point<T> 比 std::is_floating_point_v<T> 更适合出现在模板接口中。
- 编译错误会更接近“约束不满足”,而不是 SFINAE 替换失败。
3.3 算术类型
标准库没有直接提供 std::arithmetic concept,可以自己组合。
旧写法:
1 2 3 4 5 6 7 8 9 10
| template <class T> inline constexpr bool is_arithmetic_like_v = std::is_arithmetic_v<T>;
template < class T, std::enable_if_t<is_arithmetic_like_v<T>, int> = 0 > T square(T value) { return value * value; }
|
新写法:
1 2 3 4 5 6 7
| template <class T> concept Arithmetic = std::integral<T> || std::floating_point<T>;
template <Arithmetic T> T square(T value) { return value * value; }
|
对比:
- 旧写法更像给 trait 取别名。
- 新写法把组合约束作为一等接口,后续可复用。
3.4 枚举类型
旧写法:
1 2 3 4 5 6 7
| template < class T, std::enable_if_t<std::is_enum_v<T>, int> = 0 > auto to_underlying_old(T value) { return static_cast<std::underlying_type_t<T>>(value); }
|
新写法:
1 2 3 4 5 6 7
| template <class T> concept Enum = std::is_enum_v<T>;
template <Enum T> auto to_underlying_new(T value) { return static_cast<std::underlying_type_t<T>>(value); }
|
对比:
- Concepts 不要求必须完全替代类型萃取。
- 很多类型类别判断本质上仍然来自
<type_traits>,可以直接包装成 concept。
3.5 指针类型
旧写法:
1 2 3 4 5 6 7
| template < class T, std::enable_if_t<std::is_pointer_v<T>, int> = 0 > auto deref_old(T ptr) -> decltype(*ptr) { return *ptr; }
|
新写法:
1 2 3 4 5 6 7
| template <class T> concept Pointer = std::is_pointer_v<T>;
template <Pointer T> auto deref_new(T ptr) -> decltype(*ptr) { return *ptr; }
|
对比:
- 新写法把“只接受指针”的意图放在函数签名里。
- 如果以后想支持智能指针,应改用表达式约束,而不是继续检查
is_pointer。
4. 类型关系约束
这一组用于限制多个类型之间的关系,例如相同、可转换、有继承关系。
4.1 类型相同
旧写法:
1 2 3 4 5 6 7 8
| template < class T, class U, std::enable_if_t<std::is_same_v<T, U>, int> = 0 > bool same_type_equal_old(const T& lhs, const U& rhs) { return lhs == rhs; }
|
新写法:
1 2 3 4 5
| template <class T, class U> requires std::same_as<T, U> bool same_type_equal_new(const T& lhs, const U& rhs) { return lhs == rhs; }
|
对比:
std::same_as<T, U> 是标准 concept。
requires 子句适合表达两个或多个模板参数之间的关系。
4.2 可转换
旧写法:
1 2 3 4 5 6 7 8
| template < class From, class To, std::enable_if_t<std::is_convertible_v<From, To>, int> = 0 > To convert_old(From value) { return value; }
|
新写法:
1 2 3 4 5
| template <class From, class To> requires std::convertible_to<From, To> To convert_new(From value) { return value; }
|
对比:
std::convertible_to 表达语义比 std::is_convertible_v 更明确。
- 标准 concept 通常还会考虑语义一致性,而不仅是语法是否可行。
4.3 派生关系
旧写法:
1 2 3 4 5 6 7 8 9
| struct Base {};
template < class T, std::enable_if_t<std::is_base_of_v<Base, T>, int> = 0 > void handle_old(const T& value) { (void)value; }
|
新写法:
1 2 3 4 5 6 7
| struct Base {};
template <class T> requires std::derived_from<T, Base> void handle_new(const T& value) { (void)value; }
|
对比:
std::derived_from<Derived, Base> 的参数顺序很直观。
std::derived_from 比裸 is_base_of 更适合描述公开继承接口。
5. 表达式能力约束
这一组不关心类型属于什么类别,而关心它能不能做某件事。
5.1 支持加法
旧写法:用 std::void_t 检测表达式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| template <class, class = void> struct has_plus : std::false_type {};
template <class T> struct has_plus<T, std::void_t<decltype(std::declval<T>() + std::declval<T>())>> : std::true_type {};
template < class T, std::enable_if_t<has_plus<T>::value, int> = 0 > T add_old(T lhs, T rhs) { return lhs + rhs; }
|
新写法:requires 表达式。
1 2 3 4 5 6 7 8 9
| template <class T> concept Addable = requires(T lhs, T rhs) { lhs + rhs; };
template <Addable T> T add_new(T lhs, T rhs) { return lhs + rhs; }
|
对比:
- 旧写法需要额外定义检测 trait。
- 新写法直接写出要求存在的表达式。
5.2 支持相等比较
旧写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| template <class, class = void> struct has_equal : std::false_type {};
template <class T> struct has_equal<T, std::void_t<decltype(std::declval<T>() == std::declval<T>())>> : std::true_type {};
template < class T, std::enable_if_t<has_equal<T>::value, int> = 0 > bool equal_old(const T& lhs, const T& rhs) { return lhs == rhs; }
|
新写法:优先使用标准 concept。
1 2 3 4
| template <std::equality_comparable T> bool equal_new(const T& lhs, const T& rhs) { return lhs == rhs; }
|
对比:
- 旧写法只检查
== 这个表达式是否存在。
std::equality_comparable 表达的是类型满足相等比较语义,通常比手写检测更合适。
5.3 支持小于比较
旧写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| template <class, class = void> struct has_less : std::false_type {};
template <class T> struct has_less<T, std::void_t<decltype(std::declval<T>() < std::declval<T>())>> : std::true_type {};
template < class T, std::enable_if_t<has_less<T>::value, int> = 0 > const T& min_old(const T& lhs, const T& rhs) { return rhs < lhs ? rhs : lhs; }
|
新写法:
1 2 3 4
| template <std::totally_ordered T> const T& min_new(const T& lhs, const T& rhs) { return rhs < lhs ? rhs : lhs; }
|
对比:
- 如果只要求
< 存在,可以自定义 concept。
- 如果需要完整排序语义,
std::totally_ordered 更准确。
5.4 支持函数调用
旧写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| template <class F, class Arg, class = void> struct is_callable_with : std::false_type {};
template <class F, class Arg> struct is_callable_with< F, Arg, std::void_t<decltype(std::declval<F>()(std::declval<Arg>()))> > : std::true_type {};
template < class F, class Arg, std::enable_if_t<is_callable_with<F, Arg>::value, int> = 0 > auto call_old(F f, Arg arg) { return f(arg); }
|
新写法:
1 2 3 4 5
| template <class F, class Arg> requires std::invocable<F, Arg> auto call_new(F f, Arg arg) { return f(arg); }
|
对比:
std::invocable 是标准库提供的调用能力约束。
- 可读性明显高于手写
decltype(std::declval<F>()(...))。
5.5 支持下标访问
旧写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| template <class, class = void> struct has_index : std::false_type {};
template <class T> struct has_index<T, std::void_t<decltype(std::declval<T>()[std::size_t{}])>> : std::true_type {};
template < class T, std::enable_if_t<has_index<T>::value, int> = 0 > decltype(auto) first_old(T& value) { return value[0]; }
|
新写法:
1 2 3 4 5 6 7 8 9
| template <class T> concept Indexable = requires(T value) { value[std::size_t{}]; };
template <Indexable T> decltype(auto) first_new(T& value) { return value[0]; }
|
对比:
- 表达式约束适合描述容器、代理对象、视图等“能做什么”。
- 不需要为了每个表达式写一套
has_xxx trait。
6. 范围与迭代器约束
这一组用于替代过去对 begin、end、iterator category 的手写检测。
6.1 判断是否是 range
旧写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| template <class, class = void> struct is_range : std::false_type {};
template <class T> struct is_range< T, std::void_t< decltype(std::begin(std::declval<T&>())), decltype(std::end(std::declval<T&>())) > > : std::true_type {};
template < class R, std::enable_if_t<is_range<R>::value, int> = 0 > auto count_old(R&& range) { return std::distance(std::begin(range), std::end(range)); }
|
新写法:
1 2 3 4
| template <std::ranges::range R> auto count_new(R&& range) { return std::ranges::distance(range); }
|
对比:
- 旧写法只检测
begin/end 存在。
std::ranges::range 直接表达 range 概念,并能和 ranges 算法配合。
6.2 随机访问 range
旧写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| template <class R> using iterator_category_t = typename std::iterator_traits< decltype(std::begin(std::declval<R&>())) >::iterator_category;
template < class R, std::enable_if_t< std::is_base_of_v< std::random_access_iterator_tag, iterator_category_t<R> >, int > = 0 > decltype(auto) middle_old(R& range) { return range[range.size() / 2]; }
|
新写法:
1 2 3 4
| template <std::ranges::random_access_range R> decltype(auto) middle_new(R& range) { return range[std::ranges::size(range) / 2]; }
|
对比:
- 旧写法依赖 iterator category,容易被复杂迭代器和 ranges 视图影响。
- 新写法直接约束“随机访问 range”。
6.3 可排序 range
旧写法:
1 2 3 4 5 6 7 8 9 10 11
| template < class R, std::enable_if_t< is_range<R>::value && has_less<typename R::value_type>::value, int > = 0 > void sort_old(R& range) { std::sort(std::begin(range), std::end(range)); }
|
新写法:
1 2 3 4 5 6
| template <class R> requires std::ranges::random_access_range<R> && std::sortable<std::ranges::iterator_t<R>> void sort_new(R& range) { std::ranges::sort(range); }
|
对比:
- 旧写法把 range、元素比较、迭代器能力混在一起。
- 新写法分别表达随机访问和可排序两个条件。
7. 模板重载与分派
这一组用于替代 enable_if 控制重载候选。
7.1 整数和浮点分别重载
旧写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| template < class T, std::enable_if_t<std::is_integral_v<T>, int> = 0 > std::string describe_old(T) { return "integral"; }
template < class T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0 > std::string describe_old(T) { return "floating point"; }
|
新写法:
1 2 3 4 5 6 7 8 9
| template <std::integral T> std::string describe_new(T) { return "integral"; }
template <std::floating_point T> std::string describe_new(T) { return "floating point"; }
|
对比:
- Concepts 约束参与重载决议。
- 签名更短,意图更直接。
7.2 更具体的 concept 优先
旧写法:常需要额外排除条件,避免重载冲突。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| template < class T, std::enable_if_t<std::is_arithmetic_v<T>, int> = 0 > std::string category_old(T) { return "arithmetic"; }
template < class T, std::enable_if_t<std::is_integral_v<T>, long> = 0 > std::string category_integral_old(T) { return "integral"; }
|
新写法:约束可以形成偏序,更具体的约束更优。
1 2 3 4 5 6 7 8 9 10 11 12
| template <class T> concept Arithmetic = std::integral<T> || std::floating_point<T>;
template <Arithmetic T> std::string category_new(T) { return "arithmetic"; }
template <std::integral T> std::string category_new(T) { return "integral"; }
|
对比:
- Concepts 可以让更具体的约束在重载中胜出。
- 约束之间是否能正确偏序,取决于 concept 的定义方式。尽量用已有 concept 组合,不要在多个 concept 中重复写复杂布尔表达式。
7.3 用 requires 约束成员函数
旧写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| template <class T> class BoxOld { public: explicit BoxOld(T value) : value_(value) {}
template < class U = T, std::enable_if_t<std::is_integral_v<U>, int> = 0 > U increment() const { return value_ + 1; }
private: T value_; };
|
新写法:
1 2 3 4 5 6 7 8 9 10 11 12
| template <class T> class BoxNew { public: explicit BoxNew(T value) : value_(value) {}
T increment() const requires std::integral<T> { return value_ + 1; }
private: T value_; };
|
对比:
- 旧写法为了 SFINAE 常引入一个默认模板参数
U = T。
- 新写法可以直接约束非模板成员函数。
8. 类模板约束
这一组用于限制类模板参数,而不是只限制函数模板。
8.1 只允许数字类型的类模板
旧写法:
1 2 3 4 5 6 7 8 9 10 11
| template < class T, class = std::enable_if_t<std::is_arithmetic_v<T>> > class NumericValueOld { public: explicit NumericValueOld(T value) : value_(value) {}
private: T value_; };
|
新写法:
1 2 3 4 5 6 7 8 9 10 11
| template <class T> concept Numeric = std::integral<T> || std::floating_point<T>;
template <Numeric T> class NumericValueNew { public: explicit NumericValueNew(T value) : value_(value) {}
private: T value_; };
|
对比:
- 类模板约束放在模板参数位置最清晰。
- 用户实例化错误时,concept 失败通常比
enable_if 类型不存在更容易理解。
8.2 策略类约束
旧写法:检测策略类是否有成员函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| template <class Policy, class T, class = void> struct has_apply : std::false_type {};
template <class Policy, class T> struct has_apply< Policy, T, std::void_t<decltype(std::declval<Policy>()(std::declval<T>()))> > : std::true_type {};
template < class T, class Policy, std::enable_if_t<has_apply<Policy, T>::value, int> = 0 > class ProcessorOld { public: T process(T value) { return policy_(value); }
private: Policy policy_; };
|
新写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| template <class Policy, class T> concept ProcessingPolicy = requires(Policy policy, T value) { { policy(value) } -> std::same_as<T>; };
template <class T, ProcessingPolicy<T> Policy> class ProcessorNew { public: T process(T value) { return policy_(value); }
private: Policy policy_; };
|
对比:
- 新写法不仅检查能调用,还检查返回值类型。
- concept 可以作为策略接口文档。
9. requires 表达式详解
requires 表达式适合定义自定义 concept。它可以包含四类要求。
9.1 简单要求
只要求表达式合法。
旧写法:
1 2 3 4 5 6
| template <class, class = void> struct has_clear : std::false_type {};
template <class T> struct has_clear<T, std::void_t<decltype(std::declval<T&>().clear())>> : std::true_type {};
|
新写法:
1 2 3 4
| template <class T> concept Clearable = requires(T value) { value.clear(); };
|
对比:
- 简单要求用于表达“这个表达式能编译”。
- 不检查返回值类型,也不检查异常规格。
9.2 类型要求
要求某个嵌套类型存在。
旧写法:
1 2 3 4 5 6
| template <class, class = void> struct has_value_type : std::false_type {};
template <class T> struct has_value_type<T, std::void_t<typename T::value_type>> : std::true_type {};
|
新写法:
1 2 3 4
| template <class T> concept HasValueType = requires { typename T::value_type; };
|
对比:
- 类型要求用
typename。
- 适合约束容器、traits、策略类。
9.3 复合要求
要求表达式合法,并检查返回类型或 noexcept。
旧写法:
1 2 3 4 5 6 7 8 9 10
| template <class, class = void> struct size_returns_size_t : std::false_type {};
template <class T> struct size_returns_size_t< T, std::enable_if_t< std::is_same_v<decltype(std::declval<T&>().size()), std::size_t> > > : std::true_type {};
|
新写法:
1 2 3 4
| template <class T> concept Sized = requires(T value) { { value.size() } -> std::same_as<std::size_t>; };
|
对比:
- 复合要求写法是
{ expression } -> concept;。
- 箭头后面接的是 concept,不是普通类型。
带 noexcept 的版本:
1 2 3 4
| template <class T> concept NoexceptSized = requires(T value) { { value.size() } noexcept -> std::same_as<std::size_t>; };
|
9.4 嵌套要求
要求一个布尔约束成立。
旧写法:
1 2 3 4 5
| template <class T> struct small_integral_old : std::bool_constant< std::is_integral_v<T> && sizeof(T) <= sizeof(int) > {};
|
新写法:
1 2 3 4 5
| template <class T> concept SmallIntegral = requires { requires std::integral<T>; requires sizeof(T) <= sizeof(int); };
|
也可以直接写成:
1 2
| template <class T> concept SmallIntegral2 = std::integral<T> && sizeof(T) <= sizeof(int);
|
对比:
- 简单布尔组合优先用
concept A = B && C;。
- 嵌套要求适合放在复杂
requires 表达式内部。
10. 标准库常用 Concepts
10.1 类型关系
| Concept |
替代的旧 trait |
用途 |
std::same_as<T, U> |
std::is_same_v<T, U> |
两个类型相同 |
std::derived_from<T, Base> |
std::is_base_of_v<Base, T> |
公开派生关系 |
std::convertible_to<From, To> |
std::is_convertible_v<From, To> |
可转换 |
std::common_with<T, U> |
std::common_type_t<T, U> 检测 |
有公共类型 |
10.2 类型类别
| Concept |
替代的旧 trait |
用途 |
std::integral<T> |
std::is_integral_v<T> |
整数类型 |
std::signed_integral<T> |
std::is_integral_v<T> && std::is_signed_v<T> |
有符号整数 |
std::unsigned_integral<T> |
std::is_integral_v<T> && !std::is_signed_v<T> |
无符号整数 |
std::floating_point<T> |
std::is_floating_point_v<T> |
浮点类型 |
10.3 对象与调用
| Concept |
旧写法 |
用途 |
std::movable<T> |
多个 move trait 组合 |
可移动 |
std::copyable<T> |
多个 copy trait 组合 |
可复制 |
std::semiregular<T> |
默认构造、复制、析构等组合 |
类似普通值类型 |
std::regular<T> |
semiregular + equality |
常规值类型 |
std::invocable<F, Args...> |
检测 f(args...) |
可调用 |
std::predicate<F, Args...> |
调用并返回 bool-like |
谓词 |
10.4 比较
| Concept |
旧写法 |
用途 |
std::equality_comparable<T> |
检测 == / != |
相等比较 |
std::totally_ordered<T> |
检测 < <= > >= |
全序比较 |
10.5 Ranges
| Concept |
旧写法 |
用途 |
std::ranges::range<R> |
检测 begin/end |
range |
std::ranges::sized_range<R> |
检测 size |
可 O(1) 或可得大小 |
std::ranges::input_range<R> |
iterator category 检测 |
输入 range |
std::ranges::forward_range<R> |
iterator category 检测 |
前向 range |
std::ranges::random_access_range<R> |
iterator category 检测 |
随机访问 range |
11. Concepts 与类型萃取组合
Concepts 不是要删除所有类型萃取。很多底层判断仍然来自 <type_traits>。
11.1 包装已有 trait
旧写法:
1 2 3 4 5 6 7 8 9 10 11 12 13
| template <class T> struct is_string_like : std::bool_constant< std::is_convertible_v<T, std::string_view> > {};
template < class T, std::enable_if_t<is_string_like<T>::value, int> = 0 > void log_old(T text) { }
|
新写法:
1 2 3 4 5 6 7 8 9
| #include <string_view>
template <class T> concept StringLike = std::is_convertible_v<T, std::string_view>;
template <StringLike T> void log_new(T text) { }
|
对比:
- 已经稳定存在的 trait 可以直接包装成 concept。
- 外部接口使用 concept,内部实现仍可复用 trait。
11.2 保留 trait 给旧代码使用
迁移期可以同时提供 trait 和 concept。
1 2 3 4 5 6 7 8
| template <class T> struct is_serializable : std::false_type {};
template <class T> inline constexpr bool is_serializable_v = is_serializable<T>::value;
template <class T> concept Serializable = is_serializable_v<T>;
|
使用:
1 2 3 4 5 6 7 8 9 10 11 12
| template <Serializable T> void save_new(const T& value) { }
template < class T, std::enable_if_t<is_serializable_v<T>, int> = 0 > void save_old(const T& value) { }
|
对比:
- 老接口继续用 trait,避免一次性大改。
- 新接口用 concept,提高可读性。
12. 迁移建议
12.1 优先迁移函数模板接口
旧写法:
1 2 3 4 5
| template < class T, std::enable_if_t<std::is_integral_v<T>, int> = 0 > void write_id(T id);
|
新写法:
1 2
| template <std::integral T> void write_id(T id);
|
原因:
- 函数模板是调用者最常遇到错误的地方。
- Concepts 能显著改善接口阅读体验和错误信息。
12.2 把复杂 enable_if 拆成命名 concept
旧写法:
1 2 3 4 5 6 7 8 9 10
| template < class T, std::enable_if_t< std::is_integral_v<T> && std::is_signed_v<T> && sizeof(T) >= 4, int > = 0 > void accept_old(T value);
|
新写法:
1 2 3 4 5 6
| template <class T> concept LargeSignedIntegral = std::signed_integral<T> && sizeof(T) >= 4;
template <LargeSignedIntegral T> void accept_new(T value);
|
原因:
- 命名 concept 能解释业务含义。
- 复杂约束不要散落在多个函数签名里。
12.3 表达式检测迁移到 requires
旧写法:
1 2 3 4 5 6 7 8
| template <class, class = void> struct has_serialize : std::false_type {};
template <class T> struct has_serialize< T, std::void_t<decltype(std::declval<T&>().serialize())> > : std::true_type {};
|
新写法:
1 2 3 4
| template <class T> concept HasSerialize = requires(T value) { value.serialize(); };
|
原因:
std::void_t 检测 idiom 是 Concepts 最直接的替换对象。
- 迁移后代码量通常大幅减少。
13. 常见误区
13.1 concept 不是运行时检查
Concepts 在编译期生效。
1 2
| template <std::integral T> void f(T value);
|
这不是运行时判断 value 是否为整数,而是在编译期要求 T 是整数类型。
13.2 不要用类型类别替代表达式能力
如果你真正需要的是“能解引用”,不要只检查裸指针。
较弱的写法:
1 2
| template <class T> concept Pointer = std::is_pointer_v<T>;
|
更通用的写法:
1 2 3 4
| template <class T> concept Dereferenceable = requires(T value) { *value; };
|
Dereferenceable 可以支持裸指针、智能指针、迭代器等更多类型。
13.3 不要把所有约束都写在函数签名里
不推荐:
1 2 3 4 5 6
| template <class T> requires std::integral<T> && std::signed_integral<T> && (sizeof(T) >= 4) && requires(T value) { value + 1; } void f(T value);
|
推荐:
1 2 3 4 5 6 7
| template <class T> concept StableId = std::signed_integral<T> && sizeof(T) >= 4;
template <StableId T> void f(T value);
|
命名 concept 比长条件更可维护。
14. 总结
从类型萃取迁移到 Concepts 时,可以按以下顺序推进:
- 用标准库 concepts 替代常见类型萃取:
std::integral、std::same_as、std::derived_from、std::invocable。
- 用
requires 表达式替代 std::void_t 检测 idiom。
- 把复杂布尔条件提取成命名 concept。
- 对外接口优先使用 concept,底层兼容代码可以继续保留 trait。
- 对范围和迭代器代码,优先考虑
std::ranges concepts。
核心原则:
- 类型类别约束:用标准 concepts 或包装
<type_traits>。
- 表达式能力约束:用
requires 表达式。
- 多类型关系约束:用
requires 子句。
- 业务语义约束:定义命名 concept。