Concept 是 C++20 对模板参数的命名约束。它解决了模板编程的核心痛点:错误信息晦涩、约束表达隐式、SFINAE 代码难以维护

1. 核心语法

template<typename T>
concept ConceptName = constraint_expression;

定义示例

template<typename T>
concept Addable = requires(T a, T b) {
    a + b;  // 表达式必须合法
};

template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

template<typename T>
concept Container = requires(T c) {
    typename T::value_type;       // 必须有嵌套类型
    c.begin();                    // 必须有成员函数
    c.end();
    requires std::same_as<typename T::iterator, decltype(c.begin())>;
};

2. 使用方式

四种等价写法

// 1. 模板参数约束
template<Addable T>
T add(T a, T b) { return a + b; }

// 2. requires 子句(函数声明后)
template<typename T>
    requires Addable<T>
T add(T a, T b) { return a + b; }

// 3. requires 子句(函数签名中)
template<typename T>
T add(T a, T b) requires Addable<T> { return a + b; }

// 4. 简写语法(constrained auto)
Addable auto add(Addable auto a, Addable auto b) { return a + b; }

3. requires 表达式

requires 表达式有四种形式:

template<typename T>
concept Example = requires(T t, T::value_type v) {
    // 1. 简单要求:表达式必须合法
    t.size();
    
    // 2. 类型要求:嵌套类型必须存在
    typename T::value_type;
    typename T::iterator;
    
    // 3. 复合要求:表达式结果约束
    { t.begin() } -> std::same_as<typename T::iterator>;
    { t.empty() } -> std::convertible_to<bool>;
    { t.size() } noexcept -> std::same_as<size_t>;
    
    // 4. 嵌套要求:约束组合
    requires std::default_initializable<T>;
    requires std::copy_constructible<T>;
};

复合要求语法{ expression } noexcept -> type-constraint;

  • noexcept 可选,要求表达式不抛异常
  • -> type-constraint 要求表达式结果满足约束

4. 标准 Concept 分类

<concepts> 头文件提供四类约束:

类型关系

std::same_as<T, U>           // T 和 U 是同一类型
std::derived_from<D, B>      // D 公有继承自 B
std::convertible_to<From, To> // From 可隐式转换为 To
std::common_with<T, U>       // T 和 U 有公共类型

类型属性

std::integral<T>             // 整型
std::floating_point<T>       // 浮点型
std::is_pointer<T>           // 指针
std::is_array<T>             // 数组

对象属性

std::regular<T>              // 可拷贝、可比较、可默认构造
std::semiregular<T>          // 可拷贝、可默认构造
std::trivially_copyable<T>   // 可平凡拷贝

可调用

std::invocable<F, Args...>           // F 可以用 Args... 调用
std::regular_invocable<F, Args...>   // 相等输入产生相等输出
std::predicate<F, Args...>           // 返回可转换为 bool

5. Subsumption(概念包含)

Concept 支持重载决议时的包含关系:更严格的 concept 优先匹配。

template<typename T>
concept Integral = std::integral<T>;

template<typename T>
concept SignedIntegral = Integral<T> && std::is_signed_v<T>;

// SignedIntegral 包含 Integral,更严格
// 调用 SignedIntegral 版本时,signed 类型优先匹配

template<Integral T>
void process(T x) { std::print("Integral\n"); }

template<SignedIntegral T>
void process(T x) { std::print("SignedIntegral\n"); }

process(42);    // int 是 signed,输出 SignedIntegral
process(42u);   // unsigned int,输出 Integral

规则:当 concept A 的约束蕴含 concept B(A 更严格),则 A 包含 B。重载决议时,更严格的 concept 优先。

6. 错误信息对比

SFINAE 时代的错误(数百行模板展开):

error: no match for 'operator<<' (operand types are 'std::ostream' 
{aka 'std::basic_ostream<char>'} and 'std::vector<int>')
/usr/include/c++/13/ostream:108:7: note: candidate: ...
/usr/include/c++/13/ostream:117:7: note: candidate: ...
[... 50+ lines of template instantiation backtrace ...]

Concept 时代的错误

error: cannot bind non-const lvalue reference of type 'int&' 
to an rvalue of type 'int'
note: constraints not satisfied
note: the required expression 't.size()' is invalid

编译器直接告诉你哪个约束失败,而不是展开整个模板实例化链。

7. 与 SFINAE 对比

// SFINAE:晦涩、难以组合
template<typename T, 
         typename = std::enable_if_t<std::is_integral_v<T>>>
T multiply(T a, T b) { return a * b; }

// Concept:清晰、可组合
template<std::integral T>
T multiply(T a, T b) { return a * b; }

// SFINAE 组合多个约束:噩梦
template<typename T,
         typename = std::enable_if_t<
             std::is_integral_v<T> && 
             std::is_copy_constructible_v<T>>>
void func(T);

// Concept 组合:自然
template<std::integral T>
    requires std::copy_constructible<T>
void func(T);

8. 实际应用

约束迭代器类型

template<typename T>
concept ForwardIterator = requires(T it) {
    { *it } -> std::same_as<typename T::reference>;
    { ++it } -> std::same_as<T&>;
    { it++ } -> std::same_as<T>;
    requires std::equality_comparable<T>;
};

template<ForwardIterator It>
void advance(It& it, size_t n) {
    while (n--) ++it;
}

约束可序列化类型

template<typename T>
concept Serializable = requires(std::ostream& os, const T& obj) {
    { os << obj } -> std::same_as<std::ostream&>;
};

template<Serializable T>
void save(const T& obj, const std::filesystem::path& file) {
    std::ofstream ofs(file);
    ofs << obj;
}

约束数值计算

template<typename T>
concept Arithmetic = std::integral<T> || std::floating_point<T>;

template<Arithmetic T>
constexpr T clamp(T value, T lo, T hi) {
    return value < lo ? lo : (value > hi ? hi : value);
}

9. 注意事项

1. Concept 是编译期约束

template<std::integral T>
void func(T x);

func(42);    // OK
func(3.14);  // 编译错误,不是运行时检查

2. 约束不满足时的替代方案

// 使用 requires 选择不同实现
template<typename T>
    requires std::integral<T>
T abs(T x) { return x < 0 ? -x : x; }

template<typename T>
    requires std::floating_point<T>
T abs(T x) { return std::fabs(x); }

3. 避免过度约束

// 不好:过度约束,限制了可用类型
template<typename T>
concept StrictContainer = requires(T c) {
    { c.size() } -> std::same_as<size_t>;  // 太严格
};

// 好:宽松约束,更通用
template<typename T>
concept Container = requires(T c) {
    { c.size() } -> std::convertible_to<size_t>;
};

4. Concept 与 auto 结合

// 简写语法
void process(std::integral auto x);

// 等价于
template<std::integral T>
void process(T x);

10. 与 Ranges 库结合

Ranges 库大量使用 concept 约束算法:

#include <ranges>
#include <algorithm>

// std::ranges::sort 要求 random_access_range
std::vector<int> vec = {3, 1, 4, 1, 5};
std::ranges::sort(vec);  // OK

std::list<int> lst = {3, 1, 4};
// std::ranges::sort(lst);  // 编译错误:list 不满足 random_access_range
std::ranges::sort(std::views::take(vec, 3));  // OK

常用 Ranges concept

Concept要求
rangebegin()end()
sized_rangesize() 在常数时间返回
random_access_range支持常数时间随机访问
view可移动、常数时间构造/析构

11. 前置知识:typename 与嵌套类型

在模板中使用嵌套类型(如 T::iterator)时,必须使用 typename 关键字明确告诉编译器这是一个类型而非静态成员。

为什么需要 typename

当编译器遇到 T::iterator 这样的语法时,无法确定这是一个嵌套类型还是一个静态数据成员:

template<typename T>
void process(T container) {
    T::iterator it;  // 编译错误!iterator 是类型还是静态成员?
}

编译器默认假设这是静态数据成员。使用 typename 消除歧义:

template<typename T>
void process(T container) {
    typename T::iterator it;  // 明确声明 iterator 是嵌套类型
}

必须使用 typename 的场景

场景示例
声明依赖类型的嵌套类型变量typename T::iterator it;
类型别名声明using ValueType = typename T::value_type;
函数返回类型typename T::value_type get();
模板参数传递std::same_as<typename T::iterator, It>
Concept 的复合要求{ c.begin() } -> std::same_as<typename T::iterator>;

什么是嵌套类型

嵌套类型(Nested Type) 是指在类(或结构体)内部定义的类型。

class Container {
public:
    // 嵌套类型定义(四种方式)
    using value_type = int;           // 方式1:using 别名
    typedef int* pointer;             // 方式2:typedef 别名
    struct iterator { /* ... */ };    // 方式3:内部类
    enum class Status { Ok, Error };  // 方式4:内部枚举
};

// 使用嵌套类型
Container::value_type x;      // 等同于 int x
Container::iterator it;       // Container 内部的 iterator 类型
Container::Status s;          // Container 内部的 Status 枚举

与成员变量的区别

class Container {
public:
    using value_type = int;    // 嵌套类型(类型别名)
    static int count;          // 静态成员变量(是变量,不是类型)
    int size;                  // 普通成员变量
};

Container::value_type x;      // OK:value_type 是类型
Container::count = 10;        // OK:count 是静态变量

Concept 中的两种 typename 用法

requires 表达式中,typename T::name 有两种含义:

1. 类型要求(检查嵌套类型存在)

template<typename T>
concept HasIterator = requires {
    typename T::iterator;   // 检查 T 内部是否定义了 iterator 这个类型
};

2. 类型声明(使用嵌套类型)

template<typename T>
concept Container = requires(T c) {
    // 使用 T::iterator 作为类型参与比较
    { c.begin() } -> std::same_as<typename T::iterator>;
};

不需要 typename 的情况

  1. 非依赖类型(类型已知,不依赖模板参数):
struct MyClass { using Inner = int; };
MyClass::Inner x;  // 不需要 typename
  1. 使用 auto 推导(C++11 起):
template<typename Container>
void process(const Container& c) {
    auto it = c.begin();  // 编译器自动推导,无需显式声明
}

典型编译错误

如果不使用 typename

template<typename T>
void bad(T& c) {
    T::iterator it = c.begin();  // 错误!
}

错误信息:

error: need 'typename' before 'T::iterator' because 'T' is a dependent scope

12. 总结

特性SFINAEConcept
语法晦涩清晰
错误信息冗长明确
组合性困难自然
重载决议手动自动(subsumption)

Concept 不是语法糖,而是模板元编程范式的转变:从"类型替换失败不是错误"到"类型约束显式表达"。当你写模板时,先问自己:这个类型需要满足什么条件?然后用 concept 表达它。