Professional Documents
Culture Documents
06
06
06
概念
对 C++ 来说,泛型编程和使用模板的元编程已经取得了巨大的成功。但是,对泛型组件的接口却迟迟未能以一
种令人满意的方式进行合适的规范。例如,在 C++98 中,标准库算法大致是如下规定的:
```cpp
template<typename Forward_iterator, typename Value>
ForwardIterator find(Forward_iterator first, Forward_iterator last,
const Value & val)
{
while (first != last && *first != val)
++first;
return first;
}
```
C++ 标准规定:
- 第一个模板参数必须是前向迭代器。
- 第二个模板参数类型必须能够使用 `==` 与该迭代器的值类型进行比较。
- 前两个函数参数必须标示出一个序列。
这些要求是隐含在代码中的:编译器所要做的就是在函数体中使用模板参数。结果是:极大的灵活性,对正确调
用生成出色的代码,以及对不正确的调用有糟糕得一塌糊涂的错误信息。解决方案显而易见,将前两项条件作为
模板接口的一部分来指定:
```cpp
template<forward_iterator Iter, typename Value>
requires equality_comparable<Value, Iter::value_type>
forward_iterator find(Iter first, Iter last, const Value& val);
```
```cpp
template<range R, typename Value>
requires equality_comparable<Value, Range::value_type>
forward_iterator find(R r, const Value& val)
{
auto first = begin(r);
auto last = end(r);
while (first!=last && *first!=val)
++first;
return first;
}
```
为了规范模板对其参数的要求,对其提供良好支持,有过数次尝试。本节会进行描述:
- [§6.1](#61-概念的早期历史):概念的早期历史
- [§6.2](#62-c0x-概念):C++0x 中的概念
- [§6.3](#63-concepts-ts):Concepts TS
- [§6.4](#64-c20-概念):C++20 中的概念
## 6.1 概念的早期历史
- **全面的通用性/表现力**——我明确不希望这些功能只能表达我想到的东西。
- **与手工编码相比,零额外开销**——例如,我想构建一个能够与 C 语言的数组在时间和空间性能方面相当的
vector。
- **规范化的接口**——我希望类型检查和重载的功能与已有的非泛型的代码相类似。
前两个属性使模板大获成功。
```cpp
concept Value_type {
constraints(Value_type a)
{
Value_type b = a; // 拷贝初始化
a = b; // 拷贝赋值
Value_type v[] = {a}; // 不是引用
}
};
template<Value_type V>
void swap(V& a, V& b); // swap() 的参数必须是值类型
```
- 概念——用于指定对模板参数要求的编译期谓词。
- 根据使用模式来指定原始约束——以处理重载和隐式类型转换。
- 多参数概念——例如 `Mergeable<In1,In2,Out>`。
- 类型和值概念——也就是说,概念既可以将值也可以将类型当作参数,例如 `Buffer<unsigned
char,128>`。
- 模板的“类型的类型”简略写法—例如 `template<Iterator Iter> …`。
- “模板定义的简化写法”——例如 `void f(Comparable&);` 使泛型编程更接近于“普通编程”。
- `auto` 作为函数参数和返回值中约束最少的类型。
- 统一函数调用([§8.8.3](08.md#883-统一调用语法))——减少泛型编程与面向对象编程之间的风格差异
问题(例如 `x.f(y)`、`f(x,y)` 和 `x+y`)。
## 6.2 C++0x 概念
根据这些描述,这些方法似乎是不可调和的,但是对于当时的参与人员而言,这并不明显。实际上,我认为这些
方法在理论上是等效的 [Stroustrup and Dos Reis 2003b]。该论点的确可能是正确的,但对于 C++ 上
下文中的详细语言设计和使用的实际影响并不等同。另外,按照委员会成员的解释,WG21 的共识流程强烈鼓励
合作和联合提案,而不是在竞争性的提案上工作数年,最后在它们之间进行大决战([§3.2](03.md#32-组
织))。我认为后一种方法是创造方言的秘诀,因为失败的一方不太可能放弃他们的实现和用户,并就此消失。请
注意,上面提到的所有的人在一起与 Jeremy Siek(印第安纳的研究生和 AT&T 实验室的暑期实习生)
和 Jaakko Järvi(印第安那的博士后,得州农工大学教授)是 OOPSLA 论文的合著者,论文展示了折中设计
的第一个版本。印第安纳和得克萨斯的团体从未完全脱节,我们为达成真正的共识而努力。另外,从事这项工作
之前,我已经认识 Andrew Lumsdaine 很多年。我们确实希望折中方案能够正常工作。
在实现方面,印第安纳的设计的进度远远领先于得克萨斯的设计的进度,并且具有更多人员参与,所以我们主要
基于此进行。印第安纳的设计也更加符合常规,基于函数签名,并且与 Haskell 类型类有明显相似之处。考虑
到涉及的学术界人士的数量,重要的是印第安纳的设计被视为更符合常规并且学术上更为得体。看来我们“只
是”需要
- 使编译器足够快
- 生成有效的代码
- 处理重载和隐式转换。
这个决定使我们付出了三年的辛勤工作和许多争论。
概念被定义为一组操作和相关类型:
```cpp
concept EqualityComparable<typename T> {
bool operator==(const T& x, const T& y);
bool operator!=(const T& x, const T& y) { return !(x==y); }
}
某些人(印第安纳)认为概念和类之间的相似性是一种优势。
```cpp
void f(X);
void f(X&);
void f(const X&);
void f(X&&);
```
- 将 `f` 表示为一个函数,用户是否为调用选择了正确的参数?
- 是否重载了 `f` 的所有可能?
- 将 `f` 表示为一个函数,并要求用户定义一个 `concept_map`([§6.2.3](#623-概念映射))映射到
`f` 的所需的参数类型?
- 语言是否将用户的参数类型隐式映射到模板的参数类型?
传递参数的不同方式的语义并不相同,因此我们自然而然地转向接受指定的参数类型,将匹配的负担推到了类型
设计者和 `concept_maps` 的作者([§6.2.3](#623-概念映射))。
回想起来,我们对于在以特定类型的操作或特定的伪签名定义的概念框架内解决这些问题太过乐观了。“伪签
名”某种程度上代表了对此处概述的问题的解决方案。
概念之间的关系通过显式**细化**定义:
```cpp
concept BidirectionalIterator<typename Iter> // BidirectionalIterator 是
: ForwardIterator<Iter> { // 一种 ForwardIterator
// ...
}
```
细化有点像,但又不那么像类派生。这个想法是为了让程序员明确地建立概念的层次结构。不幸的是,这给系统
引入了严重的不灵活性。概念(按常规的英语含义)通常不是严格的层次结构。
### 6.2.2 概念使用
```cpp
template<typename T>
where LessThanComparable<T> // 显式谓词
const T& min(const T& x, const T& y)
{
return x<y ? x : y;
}
```cpp
concept_map EqualityComparable<int> {}; // int 满足 EqualityComparable
// student_record 满足 EqualityComparable:
concept_map EqualityComparable<student_record> {
bool operator==(const student_record& a, const student_record& b)
{
return a.id_equal(b);
}
};
```
这一直是一个争论的焦点。“印第安纳小组”一般认为明确表达意图(永远)是好的,而“得克萨斯小组”倾向
于认为除非一条概念映射能增加新的功能,写它就不只是没用,更可能有害。显式的声明是否能使用户避免因为
语义上无意义的“意外”语法匹配而导致的严重错误?还是说这种错误会很少见,显式的建模语句多半只是增加
了编写麻烦和犯错误的机会?折中的解决方案是允许在 `concept` 的定义处通过加上 `auto` 来声明使用某
条 `concept_map` 是可选的:
```cpp
auto concept EqualityComparable<typename T> {
bool operator==(const T& x, const T& y);
bool operator!=(const T& x, const T& y) { return !(x==y); }
}
```
编译器根据模板参数的概念检查模板定义中的代码:
```cpp
template<InputIterator Iter, typename Val>
requires EqualityComparable<Iter::value_type,Val>
Iter find(Iter first, Iter last, Val v)
{
while (first<last && !(*first==v)) // 错误:EqualityComparable 中没有 <
++first;
return first;
}
```
### 6.2.5 教训
初始提案得到了相对迅速的批准,之后的若干年,我们忙于为初始的设计堵漏,还要应付在通用性、可实现性、
规范质量和可用性方面的意见。
很快,事情就变得很明显:为了完成从无约束的模板到使用概念的模板的转换,我们需要语言支持。在 C++0x
的设计中,这两类模板非常不同:
- 受约束模板不能调用无约束模板,因为不知道无约束模板使用什么操作,因此无法对受约束模板进行定义检查。
- 无约束模板可以调用受约束模板,但是检查必须推迟到实例化的时候,因为在那之前我们不知道无约束模板在
调用中使用什么类型。
第一个问题的解决方案是允许程序员使用 `late_check` 块,告诉编译器“别检查这些来自受约束模板的调
用” [Gregor et al. 2008]:
```cpp
template<Semigroup T>
T add(T x, T y) {
T r = x + y; // 用 Semigroup<T>::operator+
late_check {
r = x + y; // 使用在实例化的时候找到的 operator+
// (不考虑 Semigroup<T>::operator+)
}
return r;
}
```
这一“解决方案”充其量只能算是个补丁,而且有一个特殊的问题,即调用到的无约束模板中不会知道
`Semigroup` 的 `concept_map`。这样就导致一个“有趣效果”,即一个对象可以在一段程序的两个地方以
一模一样的方式被使用,但却表达不同的语义。这样一来,类型系统就以一种实在难以追踪的方式被破坏了。
随着概念的使用越来越多,语义在概念(实际上是类型和库)设计中的作用变得越来越清晰,委员会中的许多人
开始推动一种表达语义规则的机制。这并不奇怪,Alex Stepanov 喜欢说“概念全都是语义问题”。然而,大
部分人那时都像对待其他语言功能一样对待概念,他们更关心语法和命名查找规则。
```cpp
concept TotalOrdering<typename Op, typename T> {
bool operator()(Op, T, T);
axiom Antisymmetry(Op op, T x, T y) {
if (op(x, y) && op(y, x))
x <=> y;
}
axiom Transitivity(Op op, T x, T y, T z) {
if (op(x, y) && op(y, z))
op(x, z);
}
axiom Totality(Op op, T x, T y) {
op(x, y) || op(y, x);
}
}
```
奇怪的是,要让公理的概念被接受很困难。主要的反对意见似乎是,提议者们明确拒绝了让编译器针对它们所使
用的类型来对公理进行测试“以捕获错误”的想法。显然,`axiom` 就是数学意义上的公理(也就是说,是因为
你通常无法检查而允许作的一些假设),这一观念对于某些委员是陌生的。另外一些人则不相信指定公理还可以
帮助编译器以外的工具。不过,`axiom` 还是被纳入了 `concept` 规范中。
我们在概念的定义和实现上都存在明显的问题,但我们有了一套相当完整的工具,努力地试图通过使用标准库
[Gregor and Lumsdaine 2008] 和其他库中定义的概念来解决这些问题并获得经验。
- 我们仍然没有达成一致意见,在大多数情况下,到底应使用隐式还是显式建模(隐式或显式使用
`concept_map`),哪种才是正确的方法。
- 我们仍然没有达成一致意见,是要依赖概念之间隐式还是显式的关系陈述(我们是否应该以某种非常类似面向
对象的继承的方式,显式地构建“精化”关系的层次结构?)。
- 我们仍不断看到一些实例,由受概念约束的代码生成出来的代码不及无约束模板生成出来的代码。来自模板的
后期组合机会仍然显示出惊人的优势。
- 编写概念来捕获我们在泛型和非泛型 C++ 中惯于使用的每种转换和重载情况仍然很困难。
- 我们看到了越来越多的例子,这些例子中,足够复杂的 `concept_map` 和 `late_check` 的组合导致了
对类型的不一致的看法(也就是对类型系统的惊人和几乎无法追踪的破坏)。
- 标准草案中规范的复杂性吹气球般迅速膨胀,超出了所有人的预期(有 91 页,这还不包括库中对概念的使
用),我们中的一些人认为它基本上不可读。
- 用于描述标准库的概念集越来越大(大约有 125 个概念,仅 STL 就有 103 个)。
- 编译器在代码生成方面越来越好(因为 Doug Gregor 的英勇努力),但速度仍未提高。一些主要的编译器
供应商私下里向我透露,如果一个支持概念的编译器比旧的编译器慢 20% 以上,他们就不得不反对这些概念,
不管它们有多好。当时,支持概念的编译器要慢 10 倍以上。
换句话说,我们是应该将概念设计成为供少数语言专家进行细微控制的精密设备,还是供大多数程序员使用的健
壮工具?在语言特性和标准库组件的设计中,这个问题反复出现。关于类,我多年以来都听到这样的声音;某些
人认为,显然不应该鼓励大多数程序员定义类。在某些人眼里,普通的程序员(有时被戏称为“码农小明”)显
然不够聪明或没有足够的知识来使用复杂的特性和技巧。我一向强烈认为大多数程序员可以学会并用好类和概念
等特性。一旦他们做到了,他们的编程工作就变得更容易,并且他们的代码也会变得更好。整个 C++ 社区可能
需要花费数年的时间来吸取教训;但是如果做不到的话,我们——作为语言和库的设计者——就失败了。
- 尽量少使用 `concept_map`。
- 使所有 `concept_map` 隐式/自动化。
- 概念如需要 `begin(x)`,那它也得接受 `x.begin()`,反之亦然(统一函数调用);([§6.1](#61-概
念的早期历史)),([§8.8.3](08.md#883-统一调用语法))
- 使所有标准库概念隐式/自动化。
这篇论文非常详细地包含了多年来出现的许多例子和建议。
我坚持让**所有**概念都成为隐式/自动的原因之一是观察到,如果给一个选择,最不灵活和最不轻信的程序员
可能会强迫每个人都接受他们所选择的显式概念。库作者们表现出一种强烈的倾向,即通过使用显式的(非自动
的)概念把决策推到用户那去做,即便是对于那些最明显的选择也一样。
委员展开了一次讨论回应我的论文,焦点是,为了及时加入标准,我们是否来得及达成共识。结论也很显然,没
多大希望。我们没法同意“修补”概念让它对大多数程序员可用,同时还能(多少)及时地推出标准。这样,
“概念”,这个许多有能力的人多年工作的成果,被移出了标准草案。我对“删除概念”决定的总结
[Stroustrup 2009a,b] 比技术论文和讨论更具可读性。
当委员会以压倒多数投票赞成删除概念时(我也投票赞成删除),每个发言的人都再次确认他们想要概念。投票
只是反映出概念设计还没有准备好进行标准化。我认为问题要严重得多:委员会想要概念,但委员们对他们想要
什么样的概念没有达成一致。委员会没有一套共同的设计目标。这仍然是一个问题,也不仅仅出现在概念上。委
员之间存在着深刻的“哲学上”的分歧,特别是:
- **显式还是隐式**:为了安全和避免意外,程序员是否应该显式地说明如何从潜在可选方案中做决策?该讨论
最终涉及有关重载决策、作用域决策、类型与概念的匹配、概念之间的关系,等等。
- **专家与普通人**:关键语言和标准库工具是否应该设计为供专家使用?如果是这样,是否应该鼓励“普通程
序员”只使用有限的语言子集,是否应该为“普通程序员”设计单独的库?这个讨论出现在类、类层次结构、异
常、模板等的设计和使用的场景中。
这两种情况下,回答“是”都会使功能的设计偏向于复杂的特性,这样就需要大量的专业知识和频繁使用特殊写
法才能保证正确。从系统的角度,我倾向于站在这类论点的另一端,更多地信任普通程序员,并依靠常规语言规
则,通过编译器和其他工具进行检查以避免令人讨厌的意外。对于棘手的问题,采用显式决策的方式比起依靠
(隐式)的语言规则,程序员犯错的机会只多不少。
- 我们过分重视早期实现。我们原本应该花更多的精力来确定需求、约束、期望的使用模式,以及相对简单的实
现模型。此后,我们可以依靠使用反馈来让我们的实现逐步增强。
- 有些分歧是根本的(哲学上的),无法通过折中解决,我们必须尽早发现并阐明此类问题。
- 没有一套功能集合能做到既满足一个大型专家委员会的所有不同愿望,又不会变得过分庞大,这种膨胀会成为
实现者的难题和用户的障碍。我们必须确定核心需求,并用简单的写法来满足;对于更复杂的用法和罕见的用例,
则可以用对使用者的专业知识要求更高的功能和写法。
这些结论与概念没有什么特别的关系。它们是对大团体内的设计目标和决策过程的一般观察。
## 6.3 Concepts TS
- 概念基于编译期谓词(包括多参数谓词和值参数)。
- 以使用模式来描述原始要求 [Dos Reis 和 Stroustrup 2006](`requires` 表达式)。
- 概念可以用在一般的 `requires` 子句中,当作模板形参定义中 `typename` 的替代,也可以当作函数形
参定义中类型名的替代。
- 从类型到概念的匹配是隐式的(没有 `concept_map`)。
- 重载中概念间是隐式的关系(通过计算得出,而不需要为概念进行显式细化)。
- 没有定义检查(至少目前还没有,所以也没有 `late_check`)。
- 没有 `axiom`,但这只是因为我们不想因为一个潜在有争议的特性而让设计更加复杂、产生拖延。C++0x 的
`axiom` 也可以是一个好起点。
与 C++0x 的概念相比,这里非常强调简化概念的使用,其中的一个主要部分是不要求程序员做显式表达,而让
编译器根据明确规定的、简单的算法来解决问题。
支持由用户显式决策的人认为以上的方案重语义而轻语法,并警告会有“意外匹配”和“惊吓”。最常见的例子
是 `Forward_iterator` 与 `Input_iterator` 的区别仅在于语义:`Forward_iterator` 允许在其
序列中做多遍扫描。没有人否认这种例子的存在,但围绕这些例子的重要性以及如何解决它们的争论却没断过
(仍然很起劲)。我认为让几个罕见的复杂例子主导设计是大错特错。
Concepts TS 设计是基于这样的看法(有大量经验支持),即上面这样的例子非常罕见(特别是在精心设计的
概念中 [Stroustrup 2017]),通常被概念编写者很好地理解,而且常常可以通过在最受约束的概念上添加操
作以反映语义上的差异来解决。例如,`Forward_iterator`/`Input_iterator` 问题的一个简单解决方案
是要求 `Forward_iterator` 提供一个 `can_multipass()` 操作。此操作甚至不需要做任何事情;它存
在只是为了让概念决策机制能够检查它的存在。因此,不需要专门添加新的语言特性来解决可能出现的意外歧义。
因为这一点经常被忽视,我必须强调,概念是谓词,它们不是类或类层次结构。根本上,我们只是问某个类型一
些简单的问题,如“你是迭代器吗?”并问类型的集合关于它们的互操作的问题,如“你们之间能用 `==` 来相
互比较吗?”([§6.3.2](#632-概念使用))。使用概念时,我们只问那些可以在编译期回答的问题,不涉及
运行期求值。潜在的歧义是通过比较类型(或类型集合)所涉及的谓词来检测的,而不是让程序员写决策规则
([§6.3.2](#632-概念使用))。
出于对 C++0x 概念([§6.2.6](#626-哪里出错了))中所发生问题的敏感,我们小心翼翼地设计概念,以求
使用它们不会隐含显著的编译期开销。即使是 Andrew Sutton 的编译器的早期版本,编译使用了概念的模板
的速度也比编译使用变通方案(例如 `enable_if`([§4.5.1](04.md#451-实现技巧)))的程序要**快**。
```cpp
template<Forward_iterator Iter>
void advance(Iter p, int n)
{
p+=n; // p 前进 n 个位置
}
```
这种新设计一度被称为**轻量概念**(Concepts Lite),许多人认为它不完整,甚至没用。但是,我们很快
发现,**不**进行定义检查会带来真正的好处 [Sutton and Stroustrup 2011]。
- 有了定义检查,我们在开发过程中就没办法使用部分概念检查。在构建一个大程序的初始阶段中,不知道全部
的需求是非常常见的。部分检查可以让很多错误在早期被发现,并有助于根据早期使用的反馈逐步改进设计。
- 定义检查使得设计难以拥有稳定的接口。特别是,要往类或者函数中增加调试语句、统计收集、追踪或者“遥
测”之类的支持,就不能不改变类或函数的接口来包含相应功能。这些功能对于类或函数来说很少是根本的,而
且往往会随着时间的推移而改变。
- 当我们不使用定义检查时,现有的模板可以逐渐转换为使用概念。但是,如果我们有定义检查,一个受约束的
模板就不能使用一个无约束的模板,因为我们一般没法知道无约束的模板使用了哪些功能。另外,不管做不做定
义检查,一个无约束的模板使用一个有约束的模板都意味着后期(实例化时)检查。
> 我不能支持任何包含定义检查的概念提案。
我们最终可能会得到一种定义检查的形式,但前提是我们能够设计一种机制来避开它,以满足过渡和数据收集的
需要。这需要仔细考虑,需要进行实验。C++0x 的 `late_check` 是不够的。
```cpp
template<Sequence Seq, Number Num>
Num sum(Seq s, Num v)
{
for (const auto& x : s)
v += x;
return v;
}
```
```cpp
template<typename T>
using Value_type = typename T::value_type; // 简化的别名
```cpp
template<Forward_iterator Iter>
void advance(Iter p, int n) // 将 p 向前移动 n 个元素
{
while (n--)
++p; // 前向迭代器有 ++,但没有 + 或者 +=
}
template<Random_access_iterator Iter>
void advance(Iter p, int n) // 将 p 向前移动 n 个元素
{
p += n; // 随机迭代器有 +=
}
```
也就是说,我们应该对提供随机访问的序列使用第二个版本,对只提供前向迭代的序列使用第一个版本。
```cpp
void user(vector<int>::iterator vip, list<string>::iterator lsp)
{
advance(vip, 10); // 使用较快的 advance()
advance(lsp, 10); // 使用较慢的 advance()
}
```
编译器将这两个函数的概念分解为原始(“原子”)要求,由于前向迭代的要求是随机访问迭代要求的严格子集,
所以这个例子可以被解决。
当一个参数类型同时匹配到互相之间不是严格子集的重叠要求时,会产生歧义(编译期错误)。例如:
```cpp
template<typename T>
requires Copyable<T> && Integral<T>
T fct(T x);
template<typename T>
requires Copyable<T> && Swappable<T>
T fct(T x );
程序员唯一能利用的控制机制是在定义概念时为其增加操作。不过对于现实世界的例子来说,这似乎已经足够了。
当然,你可以定义一些只在语义上有差异的概念,这样就没有办法根据我们的纯语法概念来区分它们。然而,要
避免这样做并不困难。
通过 `requires` 表达式的使用模式可指定概念的原始要求:
```cpp
template<typename T, typename U =T>
concept Equality_comparable =
requires (T a, U b) {
{ a == b } -> bool ; // 使用 == 比较 T 和 U 得到一个 bool 值
{ a != b } -> bool ; // 使用 != 比较 T 和 U 得到一个 bool 值
};
```
```cpp
static_assert(Equality_comparable<int>); // 成功
static_assert(Equality_comparable<int,long>); // 成功
struct S { int a; };
static_assert(Equality_comparable<S>); // 失败了,因为结构体不会
// 自动生成 == 和 != 操作
```
来自 C++0x(及更早的 [Stroustrup 2003])中的关联类型(associated type)概念也得到了支持:
```cpp
template<typename S>
concept Sequence = requires(S a) {
typename Value_type<S>; // S 必须具有值类型。
typename Iterator_type<S>; // S 必须具有迭代器类型。
requires Same_type<Value_type<S>,Value_type<Iterator_type<S>>>;
requires Input_iterator<Iterator_type<S>>;
};
```
注意上面的代码有重复,这是为了可以同时接受 `a.begin()` 和 `begin(a)`。缺少统一函数调用让人头疼
([§6.1](#61-概念的早期历史))、([§8.8.3](08.md#883-统一调用语法))。
```cpp
template<Input_iterator In1, Input_iterator In2, Output_iterator Out>
requires Comparable<Value_type<In1>,Value_type<In2>>
&& Assignable<Value_type<In1>, Value_type<Out>>
&& Assignable<Value_type<In2>, Value_type<Out>>
Out merge(In1, In1, In2, In2, Out);
```
```cpp
template<Input_iterator In1, Input_iterator In2, Output_iterator Out>
requires Mergeable<In1,In2,Out>
Out merge(In1, In1, In2, In2, Out);
```
```cpp
Mergeable{In1,In2,Out} // 概念名称引导器
Out merge(In1, In1, In2, In2, Out);
```
仅仅通过尝试,你就能学到很多东西,这真是令人惊叹!同样令人惊叹的是,对于那些尚未经历过这些问题的人,
新颖的写法和解决方案在他们那里也会遭遇巨大的阻力。
许多人仍然将概念视为(无论过去和现在)类型的类型这个想法的变体。是的,只有一个类型参数的概念可以看
作是一个类型的类型,但只有最简单的用法才适合该模式。
大多数泛型函数(算法)都需要不止一个模板参数,要让这样的函数有意义,这些参数类型必须以某种方式关联
起来。因此,我们必须使用多参数概念。例如:
```cpp
template<Forward_iterator Iter, typename Val>
requires Equality_comparable<Value_type<Iter>,Val>
Forward_iterator find(Iter first, Iter last, Val v)
{
while (first!=last && *first!=v)
++first;
return first;
}
```
- 参数化(例如,用值类型来参数化的 `Iterator`)
- 某种形式的继承(例如,`Random_access_iterator` 是一个 `Forward_iterator`)
- 能对一个模板参数应用多个概念的能力(例如,一个 `Container` 的元素必须满足 `Value_type` 和
`Comparable`)
- 这三种技术的组合。
结果是非常复杂的模板参数类型约束。我们认为这种复杂性是不必要的,也无法进行管理。譬如 `x+y` 和
`y+x`,其中 `x` 和 `y` 具有不同的模板参数类型,`X` 和 `Y`。在处理各自的模板参数时,我们必须将
`X` 和 `Y` 以及 `Y` 和 `X` 进行参数化。在纯面向对象语言中,这看起来很自然。毕竟,有两种方法可以
进行 `+` 运算,一种在 `X` 的层次结构中,一种在 `Y` 的层次结构中。然而,我早在 1982 年就拒绝了
C++ 的这个解决方案。要完成这一图景,我们必须添加隐式类型转换(例如,处理 `x+2` 和 `2+x`)。而多
参数概念与 C++ 解决此类场景的方式完全吻合,并避免了大部分的复杂性。
当一个泛型用法符合类型的类型这一模式时,概念能非常优雅地支持它。
- **类型**指定了一组可以(隐式和显式)应用于对象的操作,依赖于函数声明和语言规则,并会指定对象在内
存中如何布局。
- **概念**指定了一组可以(隐式和显式)应用于对象的操作,依赖于可以反映函数声明和语言规则的使用模式,
并且不涉及对象的布局。因此,概念是一种接口。
我的理想是,能用类型的地方就能用概念,并且使用方式相同。除了定义布局外,它们非常相似。概念甚至可以
用来约束那些由其初始化器来确定其类型的变量的类型(受约束的 `auto` 变量([§4.2.1](04.md#421-
auto-和-decltype)))。例如:
```cpp
template<typename T>
concept Integer = Same<T,short> || Same<T,int> || Same<T,long>;
Integer x1 = 7;
int x2 = 9;
Integer y1 = x1+x2;
int y2 = x2+x1;
void ff()
{
f(x1);
f(x2);
}
```
### 6.3.6 改进
```cpp
// 函数风格:
template<typename T>
concept bool Sequence() { return Has_begin<T>() && Has_end<T>(); }
// 表达式风格:
template<typename T>
concept bool Sequence = Has_begin<T> && Has_end<T>;
```
我们可以愉快地使用任何一种风格,但是如果两种风格都允许的话,使用概念的用户就必须知道概念定义中使用
了哪种风格,否则无法正确使用括号。很快这就成了一个大麻烦。
函数式风格允许概念重载,但是我们只有很少的概念重载例子;于是我们决定没有概念重载也可以。因此,我们
进行了简化,只使用变量模板来表达概念。Andrew Sutton 率先全面使用了概念的表达式形式。
```cpp
// 表达式风格:
template<typename T>
concept Sequence = Has_begin<T> && Has_end<T>;
```
Concepts TS 支持在函数声明中使用概念的三种写法:
- 为通用起见,显式使用 `requires` 语句
- 简略写法,用于表示类型的类型
- 自然写法(也称为简短写法、常规写法等)
基本思想是,让程序员使用与特定声明的需求紧密匹配的写法,而不会因使用更复杂声明所需的写法而淹没该定
义。为了使程序员可以自由选择写法,尤其是允许在项目开发初期或维护阶段随着功能的变化而调整,这些风格
的写法被定义为是等效的:
```cpp
void sort(Sortable &); // 自然写法
```
等同于
```cpp
template<Sortable S> void sort(S&); // 简略写法
```
等同于
```cpp
template<typename S> requires Sortable<S> void sort(S&);
```
用户对此感到非常满意,并且倾向于在大多数声明中使用自然和简略写法。但是,有些委员会成员对自然写法感
到恐惧(“我看不出它是一个模板!”),而喜欢使用最显式的 `requires` 写法,因为它甚至可以表达最复
杂的示例(“为什么你还要比那更复杂的东西?”)。我的解释是,我们对什么是简单有两种看法:
- 我可以用最简单、最快捷的方式编写代码
- 我只需要学习一种写法
我赞成前一种观点,认为这是洋葱原则([§4.2](04.md#42-c11 简化使用))的一个很好的例子。
自然写法成为对概念强烈反对的焦点。我——还有其他人——坚持这种优雅的表达
```cpp
void sort(Sortable&); // 自然写法
```
我们看到(过去和现在)这是有用而优雅的一步,可以使泛型编程逐渐变成一种普通的编程方式,而不是一种具
有不同语法、不同源代码组织偏好(“仅头文件”)和不同编码风格(例如模板元编程([§10.5.2]
(10.md#1052-元编程)))的暗黑艺术。模块解决了源代码组织问题([§9.3.1](09.md#931-模块))。另外,
更“自然”的语法解决了人们总是抱怨的关于模板语法过于冗长和笨拙的问题,我同意这些抱怨。在设计模板时,
`template<…>` 前缀语法不是我的首选。由于人们总是担心能力不强的程序员滥用模板而引起混淆和错误,我
被迫接受了这种写法。繁重的异常处理语法(`try { … } catch ( … ) { … }`)也是类似的故事
[Stroustrup 2007]。似乎对于每个新特性,许多人都要求有**醒目**的语法来防止实际和想象中的潜在问题。
然后过一段时间后,他们又抱怨太啰嗦了。
无论如何,有为数不少的委员会成员坚持认为自然语法会导致混乱和误用,因为人们(尤其是经验不足的程序
员)不会意识到以这种方式定义的函数是模板,和其他函数并不相同。我在使用和教授概念的多年里并没有观察
到这些问题,因此我并不特别担心这样的假设性问题,但反对意见仍然非常强烈。人们就是**知道**这样的代码
很危险。主要的例子是
```cpp
void f(C&&); // 危险:C 是一个概念还是类型?
```
我也很确定,我的目标是使泛型编程尽可能地像“普通”编程,但这不是普遍共识。仍然有人认为,泛型编程超
出了绝大部分程序员的能力。但我没有看到任何证据。
> ……程序员们非常渴望能使用新的语言特性,现在正是将其交付给他们的时候了。概念化标准库需要花费时间,
相信在这个过程中不会发现概念设计有什么大的问题。我们不应该让程序员一直等待语言特性,只是因为一些假
想中的设计问题,这些问题没有证据,甚至有一些反证,很可能根本不存在。为了使世界各地的 C++ 用户受益,
让我们在 C++17 里交付概念这一语言特性吧。
在这次会议上,统一调用语法([§8.8.3](08.md#883-统一调用语法))被否决,协程([§9.3.2]
(09.md#932-协程))被转为 TS,基本上确保了 C++17 只是标准的一个小版本([§8](08.md#8-c17 大海迷
航))。
## 6.4 C++20 概念
- 五年多来,自然语法在实际教学和使用中未引起任何问题。
- 用户喜欢它。
- 没有技术上的歧义。
- 它简化了常见用法。
- 这是使泛型编程更像普通编程的动力之一。
```cpp
template<auto N> void f();
```
```cpp
template<Concept T> void f(T&); // 建议被废止
```
举例来说:
```cpp
// 几乎自然的写法:
void sort(Sortable auto& x); // x 必须 Sortable
Integral auto ch = f(val); // f(val) 的结果必须为 Integral
Integral auto add(Integral auto x, Integral auto x); // 可以用一个更宽的
// 类型来防止溢出
```
“自然写法”已重命名为“缩写语法”,虽然它不仅仅是一个缩写。
遗憾的是,对于重新引入概念名称引导器并没有达成共识([§6.3.4](#634-概念名称引导器))。缺乏足够传
统的语法是一个主要的绊脚石。同样,仍然有很多人似乎不相信其有用。
## 6.5 概念的命名