Concepts 解决的问题
先看一段没有 Concepts 的代码:
template <typename T>
void printSorted(std::vector<T>& v) {
std::sort(v.begin(), v.end());
for (const auto& x : v) {
std::cout << x << '\n';
}
}
如果传入一个 std::vector<MyStruct>,而 MyStruct 没有定义 operator<,编译器会在 std::sort 的几千行模板代码深处报错。报错信息长到你的终端需要翻十几页才找到一句「no match for operator<」。
Concepts 的本质是:在编译期检查模板参数是否满足要求,不满足就在最上层的接口处直接报错,告诉他缺了什么。
template <typename T>
concept Sortable = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>;
};
template <Sortable T>
void printSorted(std::vector<T>& v) { /* 同上 */ }
现在传 MyStruct 进去,编译器直接在 printSorted 这一行报错:「T does not satisfy constraint Sortable」。一行搞定。
从需求出发写 Concept
需求一:某个类型必须支持特定操作
template <typename T>
concept Printable = requires(std::ostream& os, const T& val) {
{ os << val } -> std::convertible_to<std::ostream&>;
};
requires 表达式里的代码不会被实际执行,编译器只检查语法是否合法。这个 concept 说:如果 os << val 能编译通过、返回类型能转成 std::ostream&,那 T 就满足 Printable。
使用:
template <Printable T>
void debugPrint(const T& val) {
std::cerr << "[DEBUG] " << val << '\n';
}
需求二:类型必须是整数
template <typename T>
concept Integral = std::is_integral_v<T>;
标准库已经提供了一堆预定义的 concept,在 <concepts> 里:std::integral、std::floating_point、std::copyable、std::movable、std::derived_from<Base> 等。不要自己重新发明。
需求三:组合多个约束
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
需求四:类型有特定成员
template <typename T>
concept HasWriteMethod = requires(T& t, std::string_view msg) {
{ t.write(msg) } -> std::same_as<void>;
};
template <typename T>
concept HasFormatMethod = requires(std::string_view level, std::string_view msg) {
{ T::format(level, msg) } -> std::convertible_to<std::string>;
};
这俩 concept 就是前面那篇 Logger Policy 的编译期接口定义。任何有 write(string_view) 方法的类自动满足 HasWriteMethod,不需要继承某个基类。
四种使用 Concepts 的方式
方式一:requires 子句(最推荐)
template <typename T>
requires std::copyable<T>
void process(T val) {
auto copy = val; // 放心用,因为 concept 已经保证 T 是可拷贝的
}
最清晰,读起来像一句话:「T 必须可拷贝」。
方式二:直接替代 typename
template <std::copyable T>
void process(T val) { /* 同上 */ }
更简洁,但只适用于单一 concept。多个 concept 要用方式一或方式三。
方式三:尾置 requires
template <typename T, typename U>
requires std::copyable<T> && std::equality_comparable_with<T, U>
bool sameValue(const T& a, const U& b) {
return a == static_cast<T>(b);
}
方式四:简写函数模板(C++20 auto 参数)
auto add(std::integral auto a, std::integral auto b) {
return a + b;
}
最简洁的写法,适合简单的工具函数。但概念复杂时还是用前三种。
实战:给组件写 Concept
回到上一篇文章里 Logger 的例子。没有 concept 时,Logger 的策略接口是「鸭子类型」——你能编译过就是合法的:
template <typename OutputPolicy, typename FormatPolicy = RawFormat>
class Logger : private OutputPolicy {
public:
void log(std::string_view level, std::string_view msg) {
auto formatted = FormatPolicy::format(level, msg);
this->write(formatted);
}
};
如果有人写了 Logger<WrongPolicy>,报错位置在 this->write(formatted),错误信息是「no member named ‘write’ in WrongPolicy」——还好,不算太糟,因为这个 Logger 很简单。
但我们加上 concept 会更好:
template <typename T>
concept OutputPolicy = requires(T& t, std::string_view msg) {
{ t.write(msg) } -> std::same_as<void>;
};
template <typename T>
concept FormatPolicy = requires(std::string_view level, std::string_view msg) {
{ T::format(level, msg) } -> std::convertible_to<std::string>;
};
template <OutputPolicy OP, FormatPolicy FP = RawFormat>
class Logger : private OP {
// 同上,但接口约束现在写在了类签名里
};
现在写 Logger<int> 会立即报错:「int does not satisfy OutputPolicy」——看报错的人不需要知道 write 不存在,他只需要知道 int 不满足 OutputPolicy 这个 concept。这就像 Java 的 implements 一样清晰了。
标准库提供的 Concept 速查
| Concept | 含义 | 例子 |
|---|---|---|
std::integral<T> |
整数类型 | int, long, size_t |
std::floating_point<T> |
浮点类型 | float, double |
std::copyable<T> |
可拷贝 | std::string, int |
std::movable<T> |
可移动 | 所有容器 |
std::derived_from<T, Base> |
T 继承自 Base | 类型检查 |
std::convertible_to<T, U> |
T 可转为 U | 返回类型检查 |
std::same_as<T, U> |
T 和 U 完全相同 | 精确匹配 |
std::invocable<F, Args...> |
F 可用 Args 调用 | 回调检查 |
std::predicate<F, Args...> |
F 返回 bool | 判断条件 |
Concept 不是银弹
Concepts 是编译期检查工具,不是运行时校验工具。它能告诉你在哪个接口层犯错了、少了什么方法,但它不会替你写方法。
另外,Concepts 不要求类型显式声明它满足某个 concept。Java 需要写 class MyType implements MyInterface,C++ 不需要——编译器自动判断你的类型有没有 concept 要求的方法。这个设计叫「结构化类型系统」,好处是侵入性为零,坏处是你引入一个方法时可能「意外地」满足了某个 concept。
下一篇
系列文章:C++ 组件化基础 | 作者:Logic