12.6概念(From C++20)
文章目录
命名需求用来约束类模板和函数模板的模板类型和非类型参数。这些是作为谓词编写的,在编译期计算这些谓词,以验证传递给模板的模板参数。概念的主要目标是使与模板相关的编译器错误更具有可读性。
编写概念时,请确保它们是在为语义建模,而不仅仅是语法建模。
1.语法
概念定义的泛型语法:
template<parameter-list>
concept conceptName = constraintsExpression;
概念表达式的语法如下:
conceptName<argumentList>
概念表达式的计算结果为真或假。如果它的计算结果为真,那么表示使用给定的模板实参为概念建模。
2.约束表达式
计算结果为布尔值的常量表达式可以直接用作概念定义的约束。但它的结果必须精确计算为一个布尔值,并且没有任何类型转换。
require表达式
语法:
requires (parameterList) {requirements;}
parameterList
为可选参数,每个requirement
必须以分号作为结尾.
简单requirement
一个简单的 requirement是一个任意的表达式语句,而不是以requires
开头。不允许使用变量声明、循环、条件语句等。并且这个表达式语句永远不会被计算;编译器也只是用于验证它是否已通过编译。
template<typename T>
concept Incrementable = requires(T x){x++; ++x;}
require表达式的参数列表用于引入位于 require 表达式主体中的命名变量。并且 require 表达式的主体不能有常规变量的声明。
类型requirement
类型 requirement 用于验证特定类型是否有效。例如,下面的概念要求特定类型T有value_type
成员∶
template <typename T>
concept C = requires { typename T::value_type; };
类型需求可以用来验证某个模板是否可以使用给定的类型进行实例化。下面是一个示例:
emplate <typename T>
concept C= requires { typename SomeTemplate<T>; };
复合requirement
复合 requirement 可以用于验证某些东西不会抛出任何异常和/或验证某个方法是否返回某个类型。语法如下:
{expression} noexcept -> type-constraint;
noexcept
和-> type-constraint
都是可选的.。例如,下面的概念验证给定类型是否具有标记为noexcept
的 swap()
方法∶
template<typename T>
concept C = requires (const T x, T y){
{x.swap(y)} noexcept;
};
type-constraint 可以是任何所谓的类型约束。类型约束(type constraint)只是一个概念的名称,它包含0个或多个模板类型参数。箭头左边表达式的类型自动作为类型约束的第一个模板类型参数进行传递。因此,类型约束的实参总是比对应概念定义的模板类型参数的数目少一个。例如,具有单一模板类型的概念定义的类型约束不需要任何模板实参;可以指定空方括号<>
,或者省略它们。这听起来可能有点棘手,但通过一个示例就可以清楚地说明这一点。以下概念验证了给定类型具有一个名为size()
的方法,该方法返回的类型可转换为 size_t
的类型。
template<typename T>
concept C = requires (const T x){
{x.size()} -> convertible_to<size_t>;
};
std::convertible_to<From, to>
是标准库在<concepts>
中预定义的概念,它有两个模板类型参数。箭头左边的表达式的类型自动作为第一个模板类型参数传递给 convertible_to
的类型约束。因此,在这种情况下,只需要指定 To 模板类型实参(本例中为 size_t
)。
嵌套requirement
template<typename T>
concept C = requires (T t){
requires sizeof(t)==4;
++t; t++; --t; t--;
};
组合概念表达式
现有的概念表达式可以使用合取(&&)和析取(||)进行组合
template<typename T>
concept IncrementableAndDecrementable = Incrementable<T> && Decrementable<T>;
3.预定义的标准概念
- 核心语言概念:
same_as
,derived_from
,convertible_to
,integral
,floating_point
,copy_constructible
- 比较概念:
equality_comparable
,total_ordered
- 对象概念:
movable
,copyable
- 可调用的概念:
invocable
,predicate
4.类型约束的auto
类型约束可用于约束用自动类型推导定义的变量,在使用函数返回类型推导时约束其返回类型,约束在简化函数模板和泛型 lambda表达式中的参数,等等。
Incrementable auto value{1};
5.类型约束和函数模板
在函数模板中使用类型约束有几种不同的语法方式。第1种是简单地使用熟悉的template<>语法,但不是使用typename(或 class)),而是使用类型约束。
template<convertible_to<bool> T>
void handle(const T& t);
另一种语法是使用require子句,示例如下:
template<typename T> requires constant_expression
void handle(const T& t);
constant_expression
可以是任何产生布尔类型的常量表达式.。例如,常量表达式可以是一个概念表达式∶
template<typename T> requires Incrementable<T>
void handle(const T& t);
或者一个预定义的标准概念∶
template<typename T> requires convertible_to<T, bool>
void handle(const T& t);
或者一个 require表达式(注意2个require关键字)∶
template<typename T> requires requires(T x){x++;++x;}
void handle(const T& t);
或者任何产生布尔值的常量表达式∶
template<typename T> requires (sizeof(T)==4)
void handle(const T& t);
或者是合取和析取的组合∶
template<typename T> requires Incrementable<T> && Decrementable<T>
void handle(const T& t);
或者类型萃取:
template<typename T> requires is_arithmetic_v<T>
void handle(const T& t);
也可以在函数头之后指定 require 子句,即所谓的后置 require 子句。
emplate <typename T>
void process(const T& t)requires Incrementable<T>;
使用类型约束的一种优雅的方式是将本章前面讨论过的简化函数模板的语法和类型约束结合起
来,从而产生以下漂亮而紧凑的语法。
void process(const Incrementable auto& t);
约束包含
可以使用不同的类型约束重载函数模板。编译器总是使用具有最具体约束的模板;更具体的约束包含/暗示较少的约束。
template <typename T>
requires std::integral<T>
void process(const T &t) {
std::cout << "integral<T>" << std::endl;
}
template <typename T>
requires (std::integral<T> && sizeof(T)==4)
void process(const T &t) {
std::cout << "integral<T> && sizeof(T)==4" << std::endl;
}
int main(){
process(int{1});
process(short{1});
}
//output
integral<T> && sizeof(T)==4
integral<T>
编译器首先通过规范化约束表达式来解析任何包容。在约束表达式的规范化过程中,所有概念表达式都会被递归地扩展它们的定义,直到结果是一个由常量布尔表达式的合取和析取组成的常量表达式。如果编译器可以证明一个规范化的约束表达式包含另一个约束表达式,那么它就包含另一个约束表达式。只考虑使用合取和析取来证明任何包容,而不是否定。
这种包容推理只在语法层面上完成,而不是语义层面。例如,sizeof(T)>4
在语义上比 sizeof(T)>=4
更具体,但在语法上前者并不会包含后者。
但是,需要注意的是,类型萃取(比如前面使用的 std::is_arithmetic
特征)在规范化期间不会被扩展。因此,如果有一个预定义的概念和一个类型萃取可用,那么应该使用这个概念而不是这个萃取。例如,使用 std::integral
概念来代替std::is_integral
类型萃取。
6.类型约束和类模板
template<std::derived_from<Class1> T>
class Class : public Class2<T>
{
//...
};
或者
template<typename T> requires std::derived_from<T, Class1>
class Class : public Class2<T>
{
//...
};
7.类型约束和类方法
可以对类模板的特定方法添加额外的约束。
在方法定义和声明中添加require语句即可。
8.类型约束和模板特化
略