欢迎光临
我们一直在努力

32. 概念(Concepts):C++20 的模板约束

文章目录

  • 引言
  • 一、Concepts 的基本语法
    • 1.1 定义一个概念
    • 1.2 使用概念的四种方式
    • 1.3 使用标准库中的 Concepts
  • 二、`requires` 表达式深入
    • 2.1 四种需求(Requirements)
    • 2.2 `requires` 表达式的真假判断
  • 三、用 Concepts 约束类模板
  • 四、Concepts 的重载解析
    • 4.1 更特化的概念优先匹配
    • 4.2 多个约束的优先级
  • 五、Concepts 与实践——用约束消除运行时检查
    • 5.1 对比:传统模板 vs Concepts 模板
    • 5.2 实战:一个类型安全的通用数值容器
  • 六、标准库 Concepts 速查
  • 总结

本系列为《C++深度修炼:基础、STL源码与多线程实战》第32篇 前置条件:理解函数模板(第25篇)、类型萃取(第29篇),了解模板的错误信息(第27篇)

引言

传统模板的错误信息让人望而生畏——一个 sort(list) 的错误能吐出 200 行日志。根本原因在于:模板参数没有"类型契约"。template <typename T> 中的 typename 只是一个占位符——没有任何约束。编译器在深度实例化(几十层嵌套模板之后)才发现 T 不支持某个操作,然后从那个最深处开始,一层层向外报告"替换失败"。

C++20 的 Concepts(概念) 改变了这一切。它让你在模板声明时就写清楚"T 必须满足什么条件":

// C++20 之前:错误在实例化深度爆发
template <typename T>
T add(T a, T b) { return a + b; }

// C++20:在签名中就约束了 T
template <std::integral T>
T add(T a, T b) { return a + b; }

add(1, 2); // ✅ int 是 integral
// add(1.5, 2.5); // ❌ 编译错误——但只有一行:"double 不满足 integral 约束"


一、Concepts 的基本语法

1.1 定义一个概念

#include <concepts>
#include <type_traits>

// 方式一:用一个编译期布尔表达式定义
template <typename T>
concept Integral = std::is_integral_v<T>;

// 方式二:用 requires 表达式定义——检查是否支持某种操作
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>; // a + b 合法且结果可转换为 T
};

// 方式三:组合已有概念
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

1.2 使用概念的四种方式

// 方式一:用 concept 代替 typename(最简洁)
template <Integral T>
T gcd(T a, T b) {
while (b != 0) {
T t = b;
b = a % b;
a = t;
}
return a;
}

// 方式二:requires 子句(最灵活——可以表达多参数约束)
template <typename T>
requires Integral<T>
T gcd2(T a, T b) {
while (b != 0) { T t = b; b = a % b; a = t; }
return a;
}

// 方式三:trailing requires(紧跟在参数列表之后)
template <typename T>
T gcd3(T a, T b) requires Integral<T> {
while (b != 0) { T t = b; b = a % b; a = t; }
return a;
}

// 方式四:requires 表达式作为参数约束
template <typename T>
requires requires(T a, T b) { // 第一个 requires 是子句,第二个是表达式
{ a + b } -> std::same_as<T>;
}
T add(T a, T b) { return a + b; }

1.3 使用标准库中的 Concepts

C++20 标准库在 <concepts> 头文件中提供了大量预定义概念:

#include <concepts>
#include <iostream>

// std::integral——整数类型
template <std::integral T>
T multiply(T a, T b) { return a * b; }

// std::floating_point——浮点类型
template <std::floating_point T>
T safe_divide(T a, T b) {
return b != 0 ? a / b : 0;
}

// std::copyable——可以拷贝的类型
template <std::copyable T>
T clone(const T &src) { return T(src); }

// std::movable——可以移动的类型
template <std::movable T>
void sink(T &&obj) {
T local(std::move(obj));
// 使用 local…
}

// std::same_as——类型完全相同
template <typename T>
requires std::same_as<T, int> || std::same_as<T, long>
T increment(T x) { return x + 1; }

// std::derived_from——是某个类的派生类
template <std::derived_from<std::iostream> T>
void process_stream(T &stream) {
// T 必须继承自 iostream
}


二、requires 表达式深入

2.1 四种需求(Requirements)

requires 表达式可以检查四种需求:

template <typename T>
concept ContainerLike = requires(T c, const T cc, size_t n) {
// 1. 简单需求——表达式是否合法(不关心类型)
c.begin(); // c.begin() 必须合法
c.end(); // c.end() 必须合法

// 2. 类型需求——某个类型是否存在
typename T::value_type; // T 必须有 value_type 类型成员
typename T::iterator; // T 必须有 iterator 类型成员

// 3. 复合需求——表达式的返回类型满足某概念
{ c.begin() } -> std::input_iterator; // begin() 返回的必须是输入迭代器
{ c.size() } -> std::convertible_to<size_t>; // size() 返回值能转为 size_t

// 4. 嵌套需求——requires 里面再套 requires
requires std::same_as<decltype(cc.size()), size_t>; // const 容器也能调 size()
};

2.2 requires 表达式的真假判断

requires 表达式本身是一个编译期布尔值——表达式合法时为 true,不合法时为 false:

static_assert(requires(int a, int b) { a + b; }); // true——int 可以加
static_assert(!requires(std::string a) { a + 1; }); // false——string + int 非法
static_assert(requires { typename std::vector<int>::iterator; }); // true

// 用于 static_assert 或者 if constexpr
if constexpr (requires(T x) { x.foo(); }) {
// 只有 T 有 foo() 时才编译这段
}


三、用 Concepts 约束类模板

#include <concepts>
#include <iostream>
#include <vector>

// 约束:T 必须是可打印的(支持 operator<<)
template <typename T>
concept Printable = requires(std::ostream &os, T val) {
{ os << val } -> std::same_as<std::ostream&>;
};

// 只有 Printable 类型才能放进 PrintableVector
template <Printable T>
class PrintableVector {
std::vector<T> data_;
public:
void push(const T &val) { data_.push_back(val); }

void print_all() const {
for (const auto &x : data_) {
std::cout << x << ' ';
}
std::cout << '\\n';
}
};

// 使用时——
struct NoStream {};

int main() {
PrintableVector<int> pv; // ✅ int 有 operator<<
pv.push(1);
pv.push(2);
pv.print_all(); // 1 2

// PrintableVector<NoStream> pv2; // ❌ 编译错误:NoStream 不满足 Printable
}


四、Concepts 的重载解析

4.1 更特化的概念优先匹配

#include <concepts>
#include <iostream>

// 通用版本——接受任意类型
template <typename T>
void classify(T) { std::cout << "other\\n"; }

// 整数版本——比通用版本更特化
template <std::integral T>
void classify(T) { std::cout << "integral\\n"; }

// 浮点版本——和整数版本互斥,都比通用版更特化
template <std::floating_point T>
void classify(T) { std::cout << "floating point\\n"; }

int main() {
classify(42); // integral(integral 比通用更特化)
classify(3.14); // floating point
classify("hello"); // other(只有通用版匹配)
}

4.2 多个约束的优先级

// 定义两个概念
template <typename T>
concept Small = sizeof(T) <= 4;

template <typename T>
concept Large = sizeof(T) > 4;

// 编译器会选择约束更强的版本
template <Small T> void optimize(T) { std::cout << "small path\\n"; }
template <Large T> void optimize(T) { std::cout << "large path\\n"; }
// template <typename T> void optimize(T) { std::cout << "fallback\\n"; }

int main() {
optimize(42); // small path(int 是 4 字节)
optimize(3.14); // large path(double 是 8 字节)
}


五、Concepts 与实践——用约束消除运行时检查

5.1 对比:传统模板 vs Concepts 模板

// C++17 及之前——运行时 static_assert 或等实例化失败
template <typename T>
T divide_ceiling(T a, T b) {
static_assert(std::is_integral_v<T>, "divide_ceiling requires integral type");
// static_assert 在实例化时才触发——但如果这行放在 cpp 文件中,可能永远不被触发
return (a + b 1) / b;
}

// C++20——编译器在选择重载时就排除非整数类型
template <std::integral T>
T divide_ceiling(T a, T b) {
return (a + b 1) / b;
}

5.2 实战:一个类型安全的通用数值容器

#include <concepts>
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>

// 定义数值概念——整数或浮点
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

// 只有数值类型才能进 MathVector
template <Numeric T>
class MathVector {
std::vector<T> data_;
public:
MathVector(std::initializer_list<T> il) : data_(il) {}

T sum() const {
return std::accumulate(data_.begin(), data_.end(), T(0));
}

T average() const {
if (data_.empty()) return T(0);
return sum() / static_cast<T>(data_.size());
}

T max() const {
return *std::max_element(data_.begin(), data_.end());
}

void print() const {
for (const auto &x : data_) std::cout << x << ' ';
std::cout << '\\n';
}
};

int main() {
MathVector<int> vi = {1, 2, 3, 4, 5};
MathVector<double> vd = {1.5, 2.5, 3.5};

std::cout << "sum: " << vi.sum() << " avg: " << vi.average() << '\\n';
std::cout << "sum: " << vd.sum() << " avg: " << vd.average() << '\\n';

// MathVector<std::string> vs; // ❌ 编译错误——string 不满足 Numeric
}


六、标准库 Concepts 速查

C++20 <concepts> 中包含的核心概念:

类型属性:

概念含义
std::integral<T> 整数类型
std::floating_point<T> 浮点类型
std::signed_integral<T> 有符号整数
std::unsigned_integral<T> 无符号整数
std::same_as<T, U> T 和 U 完全相同
std::derived_from<T, U> T 派生自 U
std::convertible_to<T, U> T 能隐式转换为 U

构造与赋值:

概念含义
std::default_initializable<T> 可默认构造
std::copy_constructible<T> 可拷贝构造
std::move_constructible<T> 可移动构造
std::copyable<T> 可拷贝(构造+赋值)
std::movable<T> 可移动(构造+赋值)
std::destructible<T> 可析构

比较:

概念含义
std::equality_comparable<T> 支持 == 和 !=
std::totally_ordered<T> 支持全部比较运算符

迭代器与函数:

概念含义
std::input_iterator<T> 输入迭代器
std::output_iterator<T, V> 输出迭代器
std::forward_iterator<T> 前向迭代器
std::bidirectional_iterator<T> 双向迭代器
std::random_access_iterator<T> 随机访问迭代器
std::invocable<F, Args…> F 可以用 Args… 调用
std::predicate<F, Args…> F 用 Args… 调用并返回 bool

总结

Concepts 是 C++20 给泛型编程带来的最大变革——它把模板从"无类型约束的泛化"升级为"有类型契约的泛化":

  • template <concept T> 直接在签名中约束类型——比 static_assert 在实例化时才报错更进一步——在重载决议阶段就排除不合格类型
  • requires 表达式可以检查四种需求——简单需求(表达式合法性)、类型需求(成员类型存在性)、复合需求(返回类型约束)、嵌套需求(递归约束)
  • 有概念约束的重载中,更特化的约束优先匹配——std::integral 优于通用 typename——重载决议更精确
  • 错误信息质的飞跃——从"在 std::sort 的第 1957 行替换失败"变成"double 不满足 integral 约束"——一行错误对应一行修复
  • 标准库提供了全面的预定义概念——<concepts> 涵盖了类型属性、构造/析构、比较、迭代器、可调用性等常见约束场景
  • 这是第 6 章的最后一篇。从第 25 篇的函数模板入门,到这一篇的 Concepts——我们走完了"从无约束泛化到有约束泛化"的完整进化路径。下一章开始,我们将进入 Effective C++ 的核心习惯——这些是从 Scott Meyers 经典著作中提炼出的"不踩坑"日常编码准则。


    动手练习:

  • 定义一个 Hashable 概念——要求类型支持 std::hash<T>(提示:检查 std::hash<T>::operator() 是否合法)
  • 用 Concepts 重写第 6 章开头的 max 函数模板——要求 T 必须支持 < 比较(使用 std::totally_ordered)
  • 写一个 SortableContainer 概念——要求容器有 begin()/end() 且返回随机访问迭代器——然后用它约束一个通用的 sort_container 函数
  • 对比错误信息:分别用 template <std::integral T> 和 template <typename T> + static_assert 约束 divide_ceiling——传入 double 看哪个报错更清晰
  • 定义一个 Drawable 概念——要求对象有 draw(Canvas&) 方法,且返回 void——然后用它约束一个 render_all 函数模板
  • 赞(0)
    未经允许不得转载:171主机测评 » 32. 概念(Concepts):C++20 的模板约束
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址