C++20 中的 Concepts 与约束的使用

在 C++20 里, Concepts 与约束(Constraints)是提高模板编程安全性和可读性的强大工具。本文将介绍它们的基本语法、常见用途以及与传统 SFINAE 的区别。

1. 什么是 Concept

Concept 是对类型(或表达式)的“契约”。它把一组要求封装成一个名字,供模板参数进行约束。

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

template<typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::convertible_to <T>;
};

上面 Integral 检查 T 是否是整型,Addable 检查类型 T 是否支持加法并返回可转换为 T 的结果。

2. 使用 Concept 约束模板

在函数或类模板的参数列表中使用 requires 子句或在参数后直接写概念。

// 直接写在参数后
template<Integral T>
T add(T a, T b) { return a + b; }

// 或使用 requires 子句
template<typename T>
requires Addable <T>
T add2(T a, T b) { return a + b; }

如果实参不满足概念,编译器会给出更易读的错误信息,而不是传统 SFINAE 产生的混乱错误。

3. 与 SFINAE 的对比

  • 可读性:Concept 让意图一目了然;SFINAE 的 std::enable_if_t 难以阅读。
  • 错误信息:Concept 产生更简洁、精准的错误提示。
  • 实现:Concept 是语言层面的支持,SFINAE 是模板元编程技巧。
  • 性能:两者在生成代码时基本相同,差异可忽略。

4. 组合与继承 Concept

Concept 可以组合,形成更细粒度的约束。

template<typename T>
concept Number = Integral <T> || std::floating_point<T>;

template<typename T>
concept SignedNumber = Number <T> && std::is_signed_v<T>;

复杂的约束可以用 requires 语句实现逻辑运算。

5. 继承式编程中的应用

在面向对象编程中,Concept 可用于限制类的接口。

template<typename T>
concept Iterator = requires(T it, typename std::iterator_traits <T>::value_type val) {
    { *it } -> std::convertible_to<typename std::iterator_traits<T>::value_type>;
    { ++it } -> std::same_as<T&>;
};

此概念可用于实现通用算法(如 std::for_each)的静态检查。

6. 实战案例:类型安全的 make_unique

C++14 的 make_unique 在使用时可能产生意外的数组类型。用 Concept 可以进一步加强。

template<typename T, typename... Args>
requires (!std::is_array_v <T> && !std::is_same_v<std::remove_cv_t<T>, void>)
std::unique_ptr <T> safe_make_unique(Args&&... args) {
    return std::make_unique <T>(std::forward<Args>(args)...);
}

此约束确保 T 不是数组,也不是 void,避免编译错误。

7. 结语

Concept 与约束为 C++20 带来了更安全、可读性更好的模板编程体验。熟练使用它们可以让代码更具自文档化特性,减少错误,并让编译器提供更友好的错误信息。未来的标准版本将继续扩展 Concepts 的功能,值得持续关注。

发表评论