Concepts
如果写过大量模板代码,你可能会遇到一些编译错误信息非常长,大海捞针般的查找错误原因。C++20 引入了 Concepts,能够解救于此。大致说来,Concepts 有如下优势:
- 对模板参数的要求和约束是接口的一部分。
- Concepts 是可被编译器执行的文档。Concepts 指定了限制条件,编译器会在编译时检查条件是否成立。
- 函数的重载和模板的特化也可以基于 Concepts 来实现。
- Concepts 可以用于函数模板、类模板、模板成员函数,还可以用于变量模板(
variable templates)和别名模板(alias templates)。 - 更友好的报错信息,编译器会直接将模板参数的要求与模板实参进行对比。
- 可以使用预定义(
predefined)的 Concepts,也可以自定义(custom)Concepts。 auto与 Concepts 的用法统一了,使用auto的地方都能使用 Concepts。- 如果一个函数使用了 Concepts,那么自动编程了一个函数模板。因此编写函数模板就和编写普通函数一样简单。
用法
有四种方式使用 Concepts。
template<typename T>
requires std::integral<T>
auto gcd1(T a, T b)
{
if (b == 0)
{
return a;
}
return gcd1(b, a % b);
}
template<typename T>
auto gcd2(T a, T b)
requires std::integral<T>
{
if (b == 0)
{
return a;
}
return gcd2(b, a % b);
}
template<std::integral T>
auto gcd3(T a, T b)
{
if (b == 0)
{
return a;
}
return gcd3(b, a % b);
}
auto gcd4(std::integral auto a, std::integral auto b)
{
if (b == 0)
{
return a;
}
return gcd4(b, a % b);
}
requires clause)
- 尾部 requires 子句(trailing requires clause)
- 受约束的模板参数(constrained template parameter)
- 简写函数模板(abbreviated function template)
上面写法有些许差异,gcd4 中参数 a 和 b 是两个独立的模板参数,类型可以不同,其他三种写法必须相同的类型。
这里面用到了预定义的 Concepts std::integral,它要求模板参数是整型(integral)类型。这个预定义 Concepts 实现很简单。
requires 字句很强大,可以对模板参数或者函数声明进行约束。关键字 requires 后面是一个编译期的谓词,也可以是一个 Concepts,还可以是 requires 表达式(requires expression)。当然,也可以结合使用。
下面例子中,谓词表达式约束的是非类型 i。
sum<100>(20),编译器会给出明确的错误信息:
Tip
一般情况下应该给 Concepts 起一个有意义的名字,便于理解和使用,而不是直接使用谓词表达式。
Concepts 可以用于函数的返回类型。
template<typename T>
requires std::integral<T>
std::integral auto gcd1(T a, T b)
{
if (b == 0)
{
return a;
}
return gcd1(b, a % b);
}
std::integral auto gcd4(std::integral auto a, std::integral auto b)
{
if (b == 0)
{
return a;
}
return gcd4(b, a % b);
}
Concepts 是一个编译期的谓词,是一个在编译期执行并返回布尔值的函数。因此可以用于 static_assert,也可以用于 if constexpr。std::three_way_comparable<T> 是一个预定义的 Concepts,后面会介绍。
if constexpr (std::three_way_comparable<double>)
{
// do something
}
static_assert(std::three_way_comparable<double>);
Concepts 可以作为类的模板参数。比如
这里std::regular 是一个预定义的 Concepts,要求 T 是一个常规类型(regular type),可以拷贝(copyable)、可以默认构造(default constructible)、可以比较相等(equality comparable)。类似于内置类型 int 的行为,可以使用 == 比较。MyVector<int> 是合法的,而 MyVector<int &> 会导致编译错误。
Concepts 可以用于模板类的成员函数,也可以用于普通类的模板成员函数。下面是一个普通类的模板成员函数的例子。
class Printer
{
public:
void PrintInt(std::integral auto i) { std::cout << i << std::endl; }
};
Printer p;
p.PrintInt(42);
// no instance of function template "Printer::PrintInt" matches the argument list
p.PrintInt(3.14);
Concepts 可以用于可变参数模板(variadic template)。下面的例子要求所有的模板参数都是整型。
template<std::integral... Args>
bool All(Args... args)
{
return (... && args);
}
template<std::integral... Args>
bool Any(Args... args)
{
return (... || args);
}
标准模板库中的 std::advance 接受不同类型的迭代器 iter 和整数 n,并将迭代器前进 n 个位置。对于不同的迭代器行为不一样,比如 std::forward_list 只支持单向前进,std::list 支持双向前进,std::vector 支持随机访问。前面的两种对象的迭代器只能一步一步向前,时间复杂度是 ,而 std::vector 的迭代器可以直接跳到目标位置,时间复杂度是 。因此 std::advance 需要根据迭代器类型来选择不同的实现。C++20 Concepts 可以很方便地实现重载能力,不同容器的迭代器类型不同,调用不同的函数模板实现。
template<std::forward_iterator I>
void advance(I &it, int n)
{}
template<std::bidirectional_iterator I>
void advance(I &it, int n)
{}
template<std::random_access_iterator I>
void advance(I &it, int n)
{}
Concepts 可以用于模板的特化。下面例子中,如果是一个 regular 类型,那么会选择特化版本的 MyVector,否则选择普通版本的 MyVector。
Concepts 可以组合起来使用,比如下面的 find,要求迭代器 Iter 是一个输入迭代器(input iterator),并且要求迭代器的值类型 value_type 是一个可以和 Val 比较相等的类型(equality comparable)。
template<typename Iter, typename Val>
requires std::input_iterator<Iter> && std::equality_comparable_with<std::iter_value_t<Iter>, Val>
Iter find(Iter b, Iter e, Val v)
{}
template<std::input_iterator Iter, typename Val>
requires std::equality_comparable_with<typename std::iterator_traits<Iter>::value_type, Val>
Iter find(Iter b, Iter e, Val v)
{}
受约束和不受约束的占位符
auto 是不受约束的占位符(unconstrained placeholder),而 Concepts 是受约束的占位符(constrained placeholder)。在任何使用 auto 的地方都可以使用 Concepts。比如下面的示例。
std::integral auto GetIntegral(int val) { return val; }
int main()
{
std::vector<int> vec { 1, 2, 3, 4, 5 };
for (std::integral auto i : vec)
{
std::cout << i << " ";
}
std::cout << '\n';
std::cout << std::boolalpha << '\n';
std::integral auto b1 = true;
std::cout << b1 << '\n';
auto b2 = true;
std::cout << b2 << '\n';
std::integral auto integ1 = GetIntegral(10);
std::cout << integ1 << '\n';
auto integ2 = GetIntegral(10);
std::cout << integ2 << '\n';
return 0;
}
在 C++14 中 auto 只能用于 lambda 表达式的参数类型,在 C++20 中 auto 可以用于函数参数类型,此时函数自动变成了模板函数,当然这里也可以使用 Concepts 来约束参数类型。下面是之前的例子和对应的不受约束的 auto 版本。之前提到过 gcd4 中参数 a 和 b 是两个独立的模板参数,类型可以不同,类似的 gcd5 中参数 a 和 b 也是两个独立的模板参数,类型可以不同。
std::integral auto gcd4(std::integral auto a, std::integral auto b)
{
if (b == 0)
{
return a;
}
return gcd4(b, a % b);
}
auto gcd5(auto a, auto b)
{
if (b == 0)
{
return a;
}
return gcd5(b, a % b);
}
gcd5<uint32_t, uint32_t>(10, 20);。
这种简写函数模板也可以重载,比如下面的例子。
void Overload(auto t) { std::cout << "auto" << std::endl; }
void Overload(std::integral auto t) { std::cout << "std::integral auto" << std::endl; }
void Overload(long l) { std::cout << "long" << std::endl; }
int main()
{
Overload(3.14);
Overload(2026);
Overload(2026L);
return 0;
}
预定义 Concepts
不要重复发明轮子,C++20 提供了很多预定义的 Concepts,方便使用。下面是一些常用的预定义 Concepts。
首先讨论一下之前使用的 std::three_way_comparable<T>,它要求类型 T 支持三向比较(three-way comparison),假定 a b 都是 T 类型的对象,那么这个 Concepts 等价于
(a <=> b == 0) == bool(a == b)为true(a <=> b != 0) == bool(a != b)为true((a <=> b) <=> 0)和(0 <=> (a <=> b))是相当的(a <=> b < 0) == bool(a < b)为true(a <=> b > 0) == bool(a > b)为true(a <=> b <= 0) == bool(a <= b)为true(a <=> b >= 0) == bool(a >= b)为true
下面是几个和类型、类型之间的关系、类型分类相关的 Concepts。
std::default_initializable<T>:类型T可以默认构造(default constructible)。std::same_as<T, U>:类型T和类型U是相同的类型。std::derived_from<T, U>:类型T是类型U的派生类。std::convertible_to<T, U>:类型T可以隐式或显式地转换为类型U。std::common_reference_with<T, U>:类型T和类型U有一个公共的引用类型C,这个类型C是编译器通过std::common_reference_t<T, U>推导出来的。std::common_with<T, U>:和std::common_reference_with<T, U>类似,不过C是编译器通过std::common_type_t<T, U>推导出来的,可能不是引用类型。std::assignable_from<T, U>:类型T可以被类型U赋值,其中T必须是一个左值引用类型(lvalue reference type),因此std::assignable_from<int, int>结果是false,而std::assignable_from<int&, int>结果是true。std::swappable<T>:类型T可以交换(swappable)。
下面一组是算术相关的 Concepts。
std::integral<T>:std::is_integral_v<T>。std::signed_integral<T>:std::integral<T> && std::is_signed_v<T>。std::unsigned_integral<T>:std::integral<T> && !std::is_signed_v<T>。std::floating_point<T>:std::is_floating_point_v<T>。
下面是一组和声明周期相关的 Concepts。
std::destructible<T>:类型T可以被销毁(destructible)。std::constructible_from<T, Args...>:类型T可以用类型Args...的参数构造(constructible)。std::default_constructible<T>:类型T可以默认构造(default constructible)。std::move_constructible<T>:类型T可以移动构造(move constructible)。std::copy_constructible<T>:类型T可以拷贝构造(copy constructible)。
下面是两个比较相关的 Concepts。
std::equality_comparable<T>:类型T可以比较相等(equality comparable)。std::total_ordering<T>:类型T可以进行全序比较(total ordering)。- 这里假定
abc都是类型T的对象,那么这个 Concepts 等价于bool(a < b)bool(a > b)bool(a == b)只有一个是true- 如果
bool(a < b)bool(b < c)都是true,那么bool(a < c)也是true bool(a > b) == bool(b < a)为truebool(a <=b) == !bool(b < a)为truebool(a >= b) == !bool(a < b)为true
下面几个和对象相关的 Concepts 稍微复杂一点,下面是它们的定义。std::is_object_v<T> 是一个类型 trait,判断类型 T 是否是对象类型(object type),要求是标量、数组、union 或者 class 类型。
template<class T>
concept movable =
std::is_object_v<T> && std::move_constructible<T> && std::assignable_from<T &, T> && std::swappable<T>;
template<class T>
concept copyable = std::copy_constructible<T> && std::movable<T> && std::assignable_from<T &, T &> &&
std::assignable_from<T &, const T &> && std::assignable_from<T &, const T>;
template<class T>
concept semiregular = std::copyable<T> && std::default_initializable<T>;
template<class T>
concept regular = std::semiregular<T> && std::equality_comparable<T>;
下面是几个调用相关的 Concepts。
std::invocable<F, Args...>:类型F可以被调用(invocable),并且参数类型是Args...。std::regular_invocable<F, Args...>:类似于std::invocable,不过要求不修改函数参数,同样的输入保持相同的输出。std::predicate<F, Args...>:类型F可以被调用,并且返回值可以转换为bool。
下面是一组迭代器相关的 Concepts,定义在 <iterator> 头文件中。
std::input_iterator<I>std::output_iterator<I, T>std::forward_iterator<I>: 继承自std::input_iterator<I>,要求迭代器可以多次遍历。支持++it和it++,并且*it是一个左值引用类型,同时支持等于(==)和不等于(!=)比较。std::bidirectional_iterator<I>: 继承自std::forward_iterator<I>,要求迭代器可以前进和后退。支持--it和it--。std::random_access_iterator<I>: 继承自std::bidirectional_iterator<I>,要求迭代器可以随机访问。支持it + nit - nit[n]it1 - it2it1 < it2等操作。std::contiguous_iterator<I>: 继承自std::random_access_iterator<I>,要求迭代器指向的元素在内存中是连续的。
下面是三个与算法相关的 Concepts。
std::permutable<T>:能够对元素进行就地重排(in-place reordering)。std::mergeable:能够将多个已排序的序列合并为一个输出序列。std::sortable:能够对元素进行排序。
ranges
下面是几个与 range 相关的 Concepts。
std::ranges::range<T>:range 表示一组元素,可以通过迭代器访问,提供了开始迭代器和结束哨兵。STL 的容器都是 range。
std::ranges::input_range std::ranges::output_range std::ranges::forward_range std::ranges::bidirectional_range std::ranges::random_access_range std::ranges::contiguous_range 分别对应迭代器的 Concepts,不再赘述。
下面是几个特殊的 range。
std::ranges::borrowed_range:保证迭代器的生命周期与 range 的生命周期不绑定,range 销毁之后迭代器仍然有效。std::ranges::common_range:保证std::ranges::begin(t)和std::ranges::end(t)返回的类型相同。传统的 C++ 迭代器都是common_range。std::ranges::sized_range:保证在 时间复杂度内通过迭代器差值获取 range 的大小。std::ranges::viewable_range:保证 range 可以通过std::views::all转换为一个 view。
std::ranges::view 是一个 range,能够常量时间拷贝、移动和赋值。
template<class T>
concept view = ranges::range<T> && std::movable<T> && ranges::enable_view<T>;
template<class T>
constexpr bool enable_view = std::derived_from<T, view_base> || /*is-derived-from-view-interface*/<T>;
struct view_base
{};
view)通常作用于某个 range 执行某种操作。视图不拥有(own)数据。视图应该是只读的、无状态的,具备相等性保持(equality-preserving)。STL 的容器时 range 但不是视图。
定义 Concepts
如果标准库没有预定义我们需要的 Concepts,我们可以自定义 Concepts。定义一个 Concepts 的语法如下:
定义开始是template 关键字,后面是模板参数列表(template parameter list),然后是 concept 关键字,后面是 Concepts 的名字,最后是一个约束表达式(constraint expression)。约束表达式可以是一个编译期的谓词,可以是以下两种形式之一:
- 其他 Concepts 或编译期谓词的逻辑组合
- 逻辑组合可以使用析取(
||)、合取(&&)、非(!)等逻辑运算符。 - 编译期谓词是一个在编译期执行并返回布尔值的函数
- 一个 requires 表达式(
requires expression) - 简单 requires(
simple requirement) - 类型 requires(
type requirement) - 组合 requires(
compound requirement) - 嵌套 requires(
nested requirement)
使用逻辑运算符组合 Concepts 或编译期谓词时要遵循短路原则,如果表达式的结果已经确定了,那么会自动停止求值。
类型萃取(type trait)是编译期的谓词,因此我们已经有了很多工具可以构建自己的 Concepts。
template<typename T>
concept Integral = std::is_integral<T>::value;
template<typename T>
concept SignedIntegral = Integral<T> && std::is_signed<T>::value;
template<typename T>
concept UnsignedIntegral = Integral<T> && !SignedIntegral<T>;
template<typename T>
concept Arithmetic = std::is_integral<T>::value || std::is_floating_point<T>::value;
Warning
定义 Concepts 的时候,不能再使用 requires 做约束了。下面是一个错误的例子。
requires 表达式的语法形式如下:
parameter-list 是一个可选的参数列表,和函数的参数列表类似,requirement-seq 是一系列 requires 语句,可以是简单 requires、类型 requires、组合 requires 或嵌套 requires。
下面是一个简单 requires 的例子,要求类型 T 可以执行 + 操作。
对于类型 requires,不得不使用 typnname 关键字。比如下面的 Concepts 要求类型 T 有一个嵌套类型 value_type,并且可以用 T 来实例化 Other 模板类。
template<typename T>
concept TypeRequirement = requires {
typename T::value_type;
typename Other<T>;
};
组合 requires 的语法如下。和简单 requires 类似,组合 requires 允许使用 noexcept 关键字,并且有一个函数类型。
template<typename T>
concept Equal = requires(T a, T b) {
{ a == b } -> std::convertible_to<bool>;
{ != b } -> std::convertible_to<bool>;
};
嵌套 requires 的语法如下。
下面这个例子给了简单写法和故意使用嵌套 requires 的写法。template<typename T>
concept UnsignedIntegral = Integral<T> && !SignedIntergral<T>;
template<typename T>
concept UnsignedIntegral = Integral<T> && requires(T) { requires !SignedIntegral<T>; };
更多示例
使用前面定义的 Equal Concepts 和嵌套 requires 的写法,可以定义 Ordering Concepts,要求类型 T 可以进行全序比较(total ordering)。
template<typename T>
concept Ordering = Equal<T> && requires(T a, T b) {
{ a <= b } -> std::convertible_to<bool>;
{ a < b } -> std::convertible_to<bool>;
{ a > b } -> std::convertible_to<bool>;
{ a >= b } -> std::convertible_to<bool>;
};
<=>),更推荐使用 std::three_way_comparable<T> 预定义的 Concepts。
requires 表达式
requires 表达式可以单独使用在需要一个编译期谓词的地方,比如 static_assert 或者 if constexpr,甚至是一个 requires 字句。先看 requires 字句的例子,第一个 requires 是字句的关键字,这里表示需要一个约束,第二个 requires 是一个 requires 表达式,表示约束的内容,是一个匿名的、临时的 Concepts。
static_assert 和 if constexpr 的例子。
struct First
{
int Count() const { return 1; }
};
struct Second
{
int Size() const { return 2; }
};
template<typename T>
int GetCount(const T &obj)
{
if constexpr (requires { obj.Count(); })
{
return obj.Count();
}
else if constexpr (requires { obj.Size(); })
{
return obj.Size();
}
else
{
return 0;
}
}
int main()
{
First first;
Second second;
static_assert(requires(First f) {
{ f.Count() } -> std::convertible_to<int>;
});
// failed
static_assert(requires(Second s) {
{ s.Count() } -> std::convertible_to<int>;
});
return 0;
}