从 C++ 基础到掌握 — 第 16 集
使用 Concepts 和 requires 表达模板要求
本博客文章以教学为目的编写。 内容可能存在错误,请结合目标编译器和标准库文档进行确认。
模板能把同一规则应用到多种类型,但不支持的类型往往会在实现深处产生冗长错误。 C++20 的 Concepts 和 requires 可以把要求写进模板的公开声明。
无约束模板的问题
template <typename T>
int GetHealth(const T& value)
{
return value.Health();
}
这个函数只能用于可调用 Health() 的类型,但签名没有表达该条件。编译器必须实例化函数体后才会发现问题。
用 Concept 命名要求
#include <concepts>
template <typename T>
concept Damageable = requires(T value, int amount)
{
{ value.Health() } -> std::convertible_to<int>;
{ value.ApplyDamage(amount) } -> std::same_as<void>;
};
template <Damageable T>
void Attack(T& target, int damage)
{
target.ApplyDamage(damage);
}
requires expression 不执行代码,只检查语法和结果类型是否有效。现在调用前就能看到 Attack 的契约,错误也会更靠近调用位置。
组合类型条件
template <typename T>
concept SmallId = std::integral<T> && (sizeof(T) <= 4);
template <SmallId T>
void WriteId(T id);
Concept 可以用逻辑运算组合 type trait 和 requires,约束还会参与重载排序。
void Print(std::integral auto value);
void Print(std::floating_point auto value);
带 Concept 的 auto 是函数模板的简写。当整数和浮点数的语义不同时,分离重载通常比在一个函数中堆叠 if constexpr 更清晰。
语法有效不等于语义正确
a < b 能编译,不代表比较一定形成一致顺序;语法上可复制,也不保证两个副本在行为上彼此独立。
标准 Concepts 除语法外还记录语义要求。自定义 Concept 应使用有意义的名称,关键语义仍需测试。
不要添加超过实现所需的约束。应表达最小且真实的契约,避免在没有提升正确性的情况下拒绝可用类型。