concept 与约束:把模板要求写在接口上 / Concepts and Constraints
concept 的目的不是让代码看起来更新。
它解决的是模板接口不清楚的问题:这个模板到底需要类型具备什么能力?
1. 没有约束时,要求藏在函数体里
看函数模板:
#include <iostream>
template <class T>
void print_size(T const& x) {
std::cout << x.size() << '\n';
}接口表面上是:
任何 T 都可以但函数体真正要求:
T 类型对象必须有 size()如果调用:
print_size(42);错误会在实例化函数体时爆出来。
也就是说,调用者直到出错才知道这个模板需要什么能力。
concept 的思路是:
把要求从函数体里搬到接口上。2. 最小 concept
#include <concepts>
#include <iostream>
#include <string>
template <class T>
concept HasSize = requires(T x) {
x.size();
};
template <HasSize T>
void print_size(T const& x) {
std::cout << x.size() << '\n';
}
int main() {
std::string text = "hello";
print_size(text);
// print_size(42);
}HasSize 的读法是:
对某个 T 类型对象 x,
表达式 x.size() 必须合法。图:
call print_size(arg)
|
v
check HasSize<type of arg>
|
+-- satisfied -> instantiate function
|
+-- not satisfied -> report constraint failure这比在函数体深处报错更接近接口错误。
3. requires 可以检查表达式
简单约束:
template <class T>
concept Addable = requires(T a, T b) {
a + b;
};它只要求表达式 a + b 合法。
如果你还想检查返回类型:
#include <concepts>
template <class T>
concept AddableToSelf = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
};读法:
a + b 必须合法
并且结果类型必须正好是 T这里的 { expr } -> constraint; 是 requires 表达式里的复合要求。
4. 使用标准 concept
标准库已经提供一些常用 concept。
例如:
#include <concepts>
#include <iostream>
void show(std::integral auto value) {
std::cout << "integer: " << value << '\n';
}std::integral 表示整数类型。
调用:
show(42);
// show(3.14); // double 不满足 integral还有:
std::floating_point
浮点类型
std::same_as<A, B>
两个类型相同
std::derived_from<D, B>
D 派生自 B
std::convertible_to<From, To>
From 可以转换为 To这些不是背单词。
它们是让接口更接近自然语义。
5. 约束参与重载选择
看代码:
#include <concepts>
#include <iostream>
void describe(auto const&) {
std::cout << "general value\n";
}
void describe(std::integral auto value) {
std::cout << "integer: " << value << '\n';
}
int main() {
describe(1);
describe(3.14);
}结果是:
1
满足 integral,选择更具体版本
3.14
不满足 integral,选择通用版本约束不是注释。
它参与编译器选择。
6. concept 不能证明所有语义
concept 能检查语法和部分类型关系。
但它不能自动证明业务语义。
例如排序需要比较操作:
template <class T>
concept LessComparable = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>;
};这能检查 a < b 是否存在。
但它不能证明 < 满足严格弱序。
严格弱序大致要求:
不能 a < a
如果 a < b,则不能 b < a
比较关系要能稳定传递
等价关系要一致这些语义需要程序员和测试保证。
所以 concept 是接口边界,不是数学证明器。
7. 不要把 concept 写得过细
糟糕的约束:
template <class T>
concept MyContainer = requires(T x) {
x.begin();
x.end();
x.size();
x.reserve(10);
x.capacity();
x.data();
x.push_back(1);
x.clear();
};这个 concept 可能把接口限制得过死。
你要问:
这个函数真正需要什么能力?
只需要遍历?
需要随机访问?
需要能添加元素?
需要元素连续存储?
需要知道大小?如果只需要遍历,就不要要求 reserve()。
这和普通函数参数设计一样:
接口应该要求足够用的最小能力。8. concept 和文档
concept 不能代替文档。
但它能让文档更有根。
例如:
template <class R>
concept StringRange = requires(R r) {
r.begin();
r.end();
};这还不够好,因为没有表达元素类型。
更完整的约束可能需要 ranges 库。
但即使如此,概念名也应该承担解释职责:
HasSize
Addable
Sortable
ReadableBuffer
OutputIterator名字不要写成:
GoodType
ValidThing
MyConcept调用者看不出真实要求。
9. 本篇总结
concept 的核心价值:
把模板要求写在接口上
让错误更早、更清楚
让重载选择更可控
让泛型代码更接近普通接口设计使用 concept 时问:
这个模板真正需要类型具备什么能力?
这个约束是否太宽或太窄?
失败时错误信息能不能帮助调用者?
concept 名字是否说明了语义?下一篇讲异常、RAII 和 noexcept。现代 C++ 不只关心“模板能不能编译”,也关心“失败时资源能不能被正确清理,接口有没有承诺不抛异常”。