第三章–3.1–概念约束定义概念

Aki 发布于 2022-11-25 173 次阅读


concept历史、

早在1987年,C++之父Bjarne Stroustrup就着手为模板参数设计合适的接口。长期以来模板参数没有任何约束,仅仅在实例化时才发现类型上的错误。他希望模板拥有以下三大特点:

  • 强大的泛化,表达力
  • 相对于手写代码做到零成本开销
  • 良好的接口

目前看来C++做到了前两点,强大的泛化与表达力具备了 “图灵完备” 的能力,能够在编译时完成大量计算任务,同时生成的代码拥有比手写更好的性能,在提供前所未有的灵活性的前提下并没有性能损失,这使得模板特性非常成功。

20世纪90年代,泛型编程因C++中的标准模板库而成为主流,开发人员也开始在库开发中广泛使用泛型编程手段。使用模板做泛型编程过程中遇到的问题是缺少良好的接口,导致编译错误信息非常难读,这困扰了开发人员好多年。除了错误信息不够友好之外,在阅读使用模板元编程的库时面对大量模板参数,在不深入实现的前提下也常常不知为何物。语言上的缺陷导致后来产生enable_if等许多变通方法。

包括C++之父在内的所有人都在寻求解决方案,尤其是标准委员会的成员希望该方案能够在C++0x即C++11标准版本落地,但直到后来的C++17版本也都没能够实现。没有人能够提出一种既满足这三种目标又能合适的融于语言,并且编译速度足够快的方法。

好在C++20起对concept特性进行了标准化,目前主流的编译器也都提供了支持。concept的名字由STL之父 Alex Stepanov 命名,将一类数据类型和它的一组操作所满足的公理集称为concept:不仅需要配从语法上满足要求,还需要从语义上满足要求。

泛型编程的关键在于高度可复用的组件必须以concept为基础进行编程,而concept尽可能匹配更多的类型,使得用户能够通过各种方式与它们灵活组合。因此,concept是泛型编程的基石。

3.1、定义概念、

这里正是给concept下定义,它是一个对类型约束的编译器谓词,给定一个类型判断其能否满足语法和语义要求,这对泛型编程而言极为重要。举个例子,给定模板参数T,对它的要求如下:

  • 1)一种迭代器类型Iterator<T>
  • 2)一种数字类型Number<T>

符号C<T>中的C就是概念,T是一个类型,它表示 “如果T满足C的所有要求,那么为真,否则为假”。

concept拥有的强大表达力并且对编译时间友好,程序员能够通过非常简单的定义一个概念,也可以借助于概念库对已有概念进行泽合。概念支持重载,能够消除对变通方案(诸如enable_if等技巧)的依赖,因此不仅大大降低了元编程的难度,同时也简化了泛型编程。

在C++中定义一个concept 的语法结构为:

template<被约束的模板参数列表>
concept 概念名 =  约束表达式;

概念被定义为约束表达式,也可以简单理解成布尔常量表达式。在实现一些简单的概念时可以复用type traits,如下所示。

template<class T>
concept integral = is_integral_v<T>;

template<class T>
concept floating_point = is_floating_point_v<T>;

这种简单的概念定义能否不依托于type traits呢?答案是不行的,根据C++20标准,概念约束不允许做特化且约束表达式在定义时处于不求值环境中,因此除了type traits之外没有更好的方式了。

在判断是否满足概念时,编译器会对概念定义的约束表达式进行求值,因此可以通过静态断言来检测类型是否满足。

如果在定义概念时约束表达式类型不为bool类型,将会发生一个编译错误,而不是返回不满足即假。

约束表达式可以通过逻辑操作符的方式进行组合来定义更复杂的类型,这种操作符有两种:合取和析取。

由于在C++语法中没有定义合取和析取的符号,而是复用逻辑与和逻辑或来分别表达合取和析取,那么它们在约束表达式中的语义相对布尔运算也就有了细微区别。

约束的合取表达式由两个约束组成,判断第一个合取是否满足要求,首先要对第一个约束进行检查,如果它不满足,整个表达式也不满足;否则,当且仅当第二个约束也满足时,整个表达式满足要求。

约束的析取表达式也同样由两个约束组成,判断一个析取是否满足要求,首先对第一个约束表达式进行检测,如果它满足,整个析取表达式满足要求,否则当且仅当第二个约束也满足时,整个表达式才满足要求。

合取与析取操作与逻辑表达式中的与或运算类型,也是一个短路操作符。在依次对每个约束进行检查时,首先检测表达式是否合法,若不合法则该约束不满足,否则进一步对约束进行求值判断是否满足。

template<class T>
concept C = is_integral_v<T> && sizeof(T) >1;

static_assert(C<int>); //true
static_assert(C<double>); //false

对于可变参数模板形成的约束表达式,既不是约束合取也不似约束析取。

template<class...Args>
concept C = (is_integral_v<class Args::type> || ...);

上述代码不是析取表达式,因此没有短路操作,它首先检测整个表达式是否合法,只要有一个模板参数没有类型成员type,整个表达式为假。若要表达 “至少有一个模板参数存在类型成员type,且类型成员为整数”,则可以添加一层间接层解决。

template<class T> // 额外的间接层
concept is = is_integral_v<class T::type>;

template<class...Args>
concept C = (is<Args> || ...);

class Test
{
public:
	using type = int;
};

static_assert(C<Test,int,string,double>); //true

由于约束表达式使用的合取与析取操作符分别与逻辑表达式的逻辑与和逻辑或相同,若要表达 “逻辑表达式” 的合法性,而不是被当成析取或合取表达式处理则需要额外的工作。

template<class T,class U>
concept C1 = is_integral_v<class T::type> || is_integral_v<class U::type>;

template<class T,class U>
concept C2 = bool(is_integral_v<class T::type> || is_integral_v<class U::type>);

class Test1
{
public:
	using type = int;
};

class Test2
{
public:
	using type = double;
};

static_assert(C1<Test1, Test2>);
static_assert(C2<Test1, Test2>);
static_assert(C2<Test1, Test2>);

概念C1中的约束表达式为析取表达式,它具有短路性质,表达 “要求存在一个模板参数拥有类型成员type且类型成员为整数”,而C2表达了一条完整的逻辑表达式:“要求两个模板参数存在类型成员type,且其中一个为整数”。

另一个比较特殊的是逻辑否定,在对概念约束进行求值的过程中,若概念约束中的模板参数替换发生错误(表达式非法),则该约束的结构为不满足,考虑下列情况。

template<class T> concept C3 = is_integral_v<class T::type>;
template<class T> concept C4 = !is_integral_v<class T::type>;

class Foo
{
public:
	using type = int;
};

static_assert(!C3<int>);   //true
static_assert(!C4<int>);   //true
static_assert(C4<Foo>);    //false

其中C3表达式 “要求类型T存在关联类型type,且关联类型为int”,C3的否定是“要求类型T不存在关联类型,或关联类型不是整数”。

根据约束否定的特殊性质,C4并不是C3的否定,C4表达的是 “要求类型T存在关联类型type,且关联类型不是int”,在断言中可以看出。

如果需要表达C3的否定 “要求类型T不存在关联类型,或关联类型不是整数”,应该定义为下面形式。

template<class T>concept C5 = !C3<T>;