原文详见:C++20: Two Extremes and the Rescue with Concepts
我们在上一篇文章中完成了 C++20 的概览。现在,是时候来探究一下细节了。有什么比概念更适合作为起点呢?

必须承认:我本人就是一个铁杆的概念粉。那么,就让我们从一个刺激的例子开始吧!
两个极端

在 C++20 之前,我们有两种完全不同的方式来考虑函数或类:可以在具体类型或泛型类型上定义函数或类。在第二种情况下,我们称它们为函数模板或类模板。那么,每种方法都有什么问题呢?
太过具体
为每个特定类型定义一个函数或类是一项相当艰巨的工作。为了避免这种负担,类型转换常常会来拯救我们,然而,这种拯救往往也是一种诅咒。
// tooSpecific.cpp
#include <iostream>
void needInt(int i)
{
std::cout << "int: " << i << std::endl;
}
int main()
{
std::cout << std::boolalpha << std::endl;
double d{1.234}; // (1)N
std::cout << "double: " << d << std::endl;
needInt(d); // (2)
std::cout << std::endl;
bool b{true}; // (3)
std::cout << "bool: " << b << std::endl;
needInt(b); // (4)
std::cout << std::endl;
}
在第一种情况下(第 1 行),我以 double 开头,以 int 结尾(第 2 行)。在第二种情况下,我以 bool 开始(第 3 行),也以 int 结束(第 4 行)。
缩窄转换
使用 double 去调用 getInt(int a) 可以会触发缩窄转换。缩窄转换是指准确性下降的转换。我认为这不是你想要的。
整体提升
但是相反的情况也不好。使用 bool 去调用getInt(int a) 会将 bool 提升为 int。震惊!许多 C++ 开发者竟然不知道 bool 类型相加中时会得到哪种类型。
template <typename T>
auto add(T first, T second){
return first + second;
}
int main(){
add(true, false);
}
C++ Insights 里告诉了你真相。

函数模板 add 的特化版本使用 int 创建了完整的返回类型(第 6 - 12 行)。
我坚信,为了方便起见,C/C++ 中需要有种神奇的转换来处理函数只接受特定类型这一事实。
那么,让我们反过来做。不写针对具体类型的代码,而写是通用的。也许,用模板编写通用代码就是我们的救命稻草。
太过通用
这是我的第一次尝试。排序是一个通用的算法。如果容器中的元素是可排序的,则排序就应该应适用于每个容器。那就让我们将 std::sort 应用于 std::list 。
// sortList.cpp
#include <algorithm>
#include <list>
int main()
{
std::list<int> myList{1, 10, 3, 2, 5};
std::sort(myList.begin(), myList.end());
}
哇偶!当我试着去编译这段小程序得到了什么?!

我不想去解读这些错误消息。到底哪里出问题了?让我们仔细看一下所使用的 std::sort 的重载签名。
template< class RandomIt >
void sort( RandomIt first, RandomIt last );
std::sort 使用了一些奇怪的参数,比如 RandomIt。RandomIt 代表一个随机访问迭代器。这就是出现大量错误消息的原因,模板因此而声名狼藉。std::list 只提供了一个双向迭代器,但是 std:sort 需要一个随机访问迭代器。std::list 的结构使这一点更加显而易见。

概念的救赎
概念是救星,因为他对模板参数施加了语义约束。
以下是 std::sort 提到的类型要求:
-
RandomIt
必须满足值可交换 (ValueSwappable) 和 遗留随机访问迭代器 (LegacyRandomAccessIterator) 的要求。 - 解引用
RandomIt
结果的类型必须满足可移动赋值 (MoveAssignable) 和可移动构造 (MoveConstructible) 的要求。 -
Compare
必须满足比较 (Compare) 的要求。
std::sort 上的类型要求就是概念。关于概念的简短介绍,请阅读我之前的文章 C++20:四大件。特别地,std::sort 需要一个遗留随机访问迭代器。我稍微修改了一下来自 cppreference.com 上的示例,以让我们来仔细看看这个概念。
template<typename It>
concept LegacyRandomAccessIterator =
LegacyBidirectionalIterator<It> && // (1)
std::totally_ordered<It> &&
requires(It i, typename std::incrementable_traits<It>::difference_type n) {
{ i += n } -> std::same_as<It&>; // (2)
{ i -= n } -> std::same_as<It&>;
{ i + n } -> std::same_as<It>;
{ n + i } -> std::same_as<It>;
{ i - n } -> std::same_as<It>;
{ i - i } -> std::same_as<decltype(n)>;
{ i[n] } -> std::convertible_to<std::iter_reference_t<It>>;
};
这就是关键所在。如果它支持概念LegacyRandomAccessIterator(第 2 行)和所有其他要求,那么它就支持这个概念。例如,第 2 行中的要求表示类型为 It: {i += n} 的值是一个有效的表达式,它返回一个 i&。总的来说就是,std::list 支持一个 LegacyBidirectionalIterator。
不容置疑,这一节的技术性很强。我们来试试。有了概念,你可以期待一个简洁的错误信息,如以下:
当然,这个错误消息是假的,因为还没有编译器实现了 C++20 语法的概念。MSVC 19.23 部分地支持它们,GCC 上是概念的一个以前的版本。cppreference.com 提供了概念当前状态的更多细节。
我提到过 GCC 支持旧版本的概念吗?
概念:从历史中走来
我第一次听说概念是在 2005 年到 2006 年之间。这让我想起了 Haskell 类型的类。Haskell 中的类型类是类似类型的接口。下面是 Haskell 类型类层次结构的一部分。

但是,C++ 的概念与之不同:
- 在 Haskell 中,类型必须是类型类的实例。在C++ 20 中,类型必须满足概念的要求。
- 概念可以用在模板的非类型参数上。例如,像 5 这样的数字是非类型参数。当你想要一个包含 5 个元素的 std::array 时,你可以使用非类型的参数 5:std::array<int, 5> myArray。
- 概念不会增加运行时开销
最初,概念应该是 C++11 的关键特性,但是在 2009 年 7 月于法兰克福举行的标准化会议上,它被删除了。引用 Bjarne Stroustrup 的话是:“C++ 0x 的概念设计演变成了一个复杂的怪物。”几年后,第二次尝试也不成功:C++17 标准中删除了概念。最中,它们终于成了 C++ 20 的一部分。
接下来?
不出所料,我的下一篇文章还是关于概念的。我会举很多例子,来说明模板参数的语义约束是什么意思。
网友评论