C++20 Concepts用法

C++ Concepts 教程:从类型萃取迁移到 Concepts

本文按用途和分类介绍 C++20 Concepts。每个用法都给出旧写法和新写法对比:

  • 旧写法:类型萃取、std::enable_ifstd::void_tstatic_assert、SFINAE。
  • 新写法:conceptrequires、标准库 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. 范围与迭代器约束

这一组用于替代过去对 beginend、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 时,可以按以下顺序推进:

  1. 用标准库 concepts 替代常见类型萃取:std::integralstd::same_asstd::derived_fromstd::invocable
  2. requires 表达式替代 std::void_t 检测 idiom。
  3. 把复杂布尔条件提取成命名 concept。
  4. 对外接口优先使用 concept,底层兼容代码可以继续保留 trait。
  5. 对范围和迭代器代码,优先考虑 std::ranges concepts。

核心原则:

  • 类型类别约束:用标准 concepts 或包装 <type_traits>
  • 表达式能力约束:用 requires 表达式。
  • 多类型关系约束:用 requires 子句。
  • 业务语义约束:定义命名 concept。
作者

echo

发布于

2026-06-24

更新于

2026-06-24

许可协议

评论