C++20 概念和约束

最后更新: 2025-12-23 21:51:39 +0800 +0800
tags: C++ | 模板
原文以英文撰写。您正在阅读AI翻译版本。

C++20 概念允许模板声明在其接口中表达可由编译器检查的要求。只有满足关联约束时,受约束候选项才参与重载解析。因此,约束可以用公开预期操作集的声明替代许多 std::enable_if SFINAE 用法;但它们不能证明 API 所记录的语义要求。

声明约束 #

概念是命名空间作用域、类似变量的模板,其定义是 bool 常量表达式:

cpp
#include <concepts>

template <class T>
concept Addable = requires(T value) {
    { value + value } -> std::convertible_to<T>;
};

可以通过 requires 子句、受约束模板参数或缩写函数模板使用概念:

cpp
template <class T>
    requires Addable<T>
T sum(T left, T right) {
    return left + right;
}

std::integral auto increment(std::integral auto value) {
    return value + 1;
}

每个 std::integral auto 都引入一个不同的模板参数。当两个实参必须具有相同的推导类型时,应使用显式参数。当约束跟在声明器之后时,尾置 requires 子句很有用,包括作为模板类成员时:

cpp
template <class T>
struct Buffer {
    void compact() requires std::movable<T>;
};

非模板自由函数不能出现 requires 子句。多个声明位置的约束使用 && 组合;应将前置条件放在依赖该前置条件的要求之前。

Requires 表达式和语义要求 #

requires 表达式会检查要求而不求值其操作数。它支持简单要求、类型要求、复合要求和嵌套要求:

cpp
#include <concepts>
#include <iterator>

template <class T>
concept SortableRange = requires(T range) {
    typename T::value_type;                         // type
    { range.begin() } -> std::input_or_output_iterator; // compound
    range.end();                                    // simple
    requires std::movable<typename T::value_type>;  // nested
};

替换后无效的要求会使依赖约束不满足,而不会在调用点产生诊断。对每一种可能的替换都无效的要求是不合式的,但不要求诊断。

满足性是句法层面的。某个类型可能满足相等表达式要求,却违反 std::equality_comparable 所要求的等价关系;某个类型可能使 swap 编译通过,却违反其规定的复杂度或后置条件。应记录并测试这些语义要求,因为编译器无法建立它们。

从 enable_if 迁移 #

应迁移现有选择策略,而不是仅仅因为概念可用就扩大重载集合:

cpp
// Before C++20.
template <class T, std::enable_if_t<std::integral<T>, int> = 0>
T twice(T value) { return value + value; }

// C++20, with the same intended eligibility.
template <std::integral T>
T twice(T value) { return value + value; }

约束选择候选项是否可行;它们不会将选定函数体中的错误变成替换失败。替换 enable_if 时,应保留排除情况、转换和排序测试,尤其是在旧重载使用默认模板参数或返回类型 SFINAE 的情况下。

排序和包含 #

当多个受约束重载都满足时,语言使用约束包含关系选择约束更强的候选项。包含关系具有结构性,基于规范化原子约束,而不是任意布尔等价。应命名共享谓词并组合它们:

cpp
template <class T>
concept Range = requires(T value) { value.begin(); value.end(); };

template <class T>
concept SizedRange = Range<T> && requires(T value) { value.size(); };

template <Range T>
void inspect(const T&);

template <SizedRange T>
void inspect(const T&); // selected when both are satisfied

重复看似含义相同的表达式可能创建不同的原子约束,使重载产生歧义。命名概念保留排序规则所需的原子约束身份。

转发引用和定制点 #

对于 T 被推导的 T&&,左值实参会将 T 推导为左值引用类型。当契约涉及底层对象时,应约束 std::remove_cvref_t<T>:

cpp
#include <ranges>
#include <type_traits>

template <class R>
    requires std::ranges::range<std::remove_cvref_t<R>>
void consume(R&& range);

只有在有意处理引用限定时,才直接约束 R。这一差异可以防止左值仅仅因为概念是针对对象类型编写的而被拒绝。

定制点对象(CPO)通常是一个 inline constexpr 函数对象,其受约束的调用运算符实现定义好的定制协议。例如,std::ranges::begin 可以从数组、合适的成员 begin 或通过依赖参数查找找到的未限定 begin 中获取迭代器。约束应在 CPO 边界描述支持的协议,使不受支持的类型在该接口处失败,而不是在选定的定制实现深处失败。

评论

在 GitHub 上阅读或评论(发表评论需要登录)