您的位置:首页 > 编程语言 > C语言/C++

C++20初体验——concepts

2020-12-05 21:54 2336 查看

引子

凡是涉及STL的错误都不堪入目,因为首先STL中有复杂的层次关系,在错误信息中都会暴露出来,其次这么多类和函数的名字大多都是双下划线开头的,一般人看得不习惯。

一个经典的错误是给

std::sort
传入
std::list<T>
的迭代器:

#include <list>
#include <algorithm>

int main()
{
std::list<int> list;
std::sort(list.begin(), list.end());
}

GCC 10.1.0给出如下错误信息(没有开

-std=c++20
):

In file included from c:\program files\mingw-w64\include\c++\10.1.0\algorithm:62,
from temp.cpp:3:
c:\program files\mingw-w64\include\c++\10.1.0\bits\stl_algo.h: In instantiation of 'void std::__sort(_RandomAccessIterator, _RandomAccessIterator, _Compare) [with _RandomAccessIterator = std::_List_iterator<int>; _Compare = __gnu_cxx::__ops::_Iter_less_iter]':
c:\program files\mingw-w64\include\c++\10.1.0\bits\stl_algo.h:4859:18:   required from 'void std::sort(_RAIter, _RAIter) [with _RAIter = std::_List_iterator<int>]'
temp.cpp:9:39:   required from here
c:\program files\mingw-w64\include\c++\10.1.0\bits\stl_algo.h:1975:22: error: no match for 'operator-' (operand types are 'std::_List_iterator<int>' and 'std::_List_iterator<int>')
1975 |     std::__lg(__last - __first) * 2,
|               ~~~~~~~^~~~~~~~~
In file included from c:\program files\mingw-w64\include\c++\10.1.0\bits\stl_algobase.h:67,
from c:\program files\mingw-w64\include\c++\10.1.0\bits\char_traits.h:39,
from c:\program files\mingw-w64\include\c++\10.1.0\ios:40,
from c:\program files\mingw-w64\include\c++\10.1.0\ostream:38,
from c:\program files\mingw-w64\include\c++\10.1.0\iostream:39,
from temp.cpp:1:
c:\program files\mingw-w64\include\c++\10.1.0\bits\stl_iterator.h:500:5: note: candidate: 'template<class _IteratorL, class _IteratorR> constexpr decltype ((__y.base() - __x.base())) std::operator-(const std::reverse_iterator<_Iterator>&, const std::reverse_iterator<_IteratorR>&)'
500 |     operator-(const reverse_iterator<_IteratorL>& __x,
|     ^~~~~~~~
c:\program files\mingw-w64\include\c++\10.1.0\bits\stl_iterator.h:500:5: note:   template argument deduction/substitution failed:
In file included from c:\program files\mingw-w64\include\c++\10.1.0\algorithm:62,
from temp.cpp:3:
c:\program files\mingw-w64\include\c++\10.1.0\bits\stl_algo.h:1975:22: note:   'std::_List_iterator<int>' is not derived from 'const std::reverse_iterator<_Iterator>'
1975 |     std::__lg(__last - __first) * 2,
|               ~~~~~~~^~~~~~~~~
In file included from c:\program files\mingw-w64\include\c++\10.1.0\bits\stl_algobase.h:67,
from c:\program files\mingw-w64\include\c++\10.1.0\bits\char_traits.h:39,
from c:\program files\mingw-w64\include\c++\10.1.0\ios:40,
from c:\program files\mingw-w64\include\c++\10.1.0\ostream:38,
from c:\program files\mingw-w64\include\c++\10.1.0\iostream:39,
from temp.cpp:1:
c:\program files\mingw-w64\include\c++\10.1.0\bits\stl_iterator.h:1533:5: note: candidate: 'template<class _IteratorL, class _IteratorR> constexpr decltype ((__x.base() - __y.base())) std::operator-(const std::move_iterator<_IteratorL>&, const std::move_iterator<_IteratorR>&)'
1533 |     operator-(const move_iterator<_IteratorL>& __x,
|     ^~~~~~~~
c:\program files\mingw-w64\include\c++\10.1.0\bits\stl_iterator.h:1533:5: note:   template argument deduction/substitution failed:
In file included from c:\program files\mingw-w64\include\c++\10.1.0\algorithm:62,
from temp.cpp:3:
c:\program files\mingw-w64\include\c++\10.1.0\bits\stl_algo.h:1975:22: note:   'std::_List_iterator<int>' is not derived from 'const std::move_iterator<_IteratorL>'
1975 |     std::__lg(__last - __first) * 2,
|               ~~~~~~~^~~~~~~~~

太长不看,加三告辞。换个Visual Studio 2019:

Severity	Code	Description	Project	File	Line	Suppression State
Error	C2676	binary '-': 'const std::_List_unchecked_iterator<std::_List_val<std::_List_simple_types<_Ty>>>' does not define this operator or a conversion to a type acceptable to the predefined operator	temp	C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.27.29110\include\algorithm	4138
Error	C2672	'_Sort_unchecked': no matching overloaded function found	temp	C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.27.29110\include\algorithm	4138
Error	C2780	'void std::_Sort_un
56c
checked(_RanIt,_RanIt,iterator_traits<_Iter>::difference_type,_Pr)': expects 4 arguments - 3 provided	temp	C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.27.29110\include\algorithm	4138

虽然错误信息简短许多,但仍不能告诉我们错误的原因(这些是内部原因)。

我们注意到两段错误都提到了

operator-
,实际上编译器认为错误在于
std::sort
中会把两个输入迭代器所属类型的实例相减,而
std::list<T>::iterator
没有重载
operator-
运算符。这当然不是让我们来重载这个运算符。

STL源码可以提供一些帮助:

/**
*  @brief Sort the elements of a sequence.
*  @ingroup sorting_algorithms
*  @param  __first   An iterator.
*  @param  __last    Another iterator.
*  @return  Nothing.
*
*  Sorts the elements in the range @p [__first,__last) in ascending order,
*  such that for each iterator @e i in the range @p [__first,__last-1),
*  *(i+1)<*i is false.
*
*  The relative ordering of equivalent elements is not preserved, use
*  @p stable_sort() if this is needed.
*/
template<typename _RandomAccessIterator>
_GLIBCXX20_CONSTEXPR
inline void
sort(_RandomAccessIterator __first, _RandomAccessIterato
ad0
r __last)
{
// concept requirements
__glibcxx_function_requires(_Mutable_RandomAccessIteratorConcept<
_RandomAccessIterator>)
__glibcxx_function_requires(_LessThanComparableConcept<
typename iterator_traits<_RandomAccessIterator>::value_type>)
__glibcxx_requires_valid_range(__first, __last);
__glibcxx_requires_irreflexive(__first, __last);

std::__sort(__first, __last, __gnu_cxx::__ops::__iter_less_iter());
}

在概念上(conceptually),

std::list<T>
的迭代器不满足RandomAccessIterator的要求,所以不能用于
std::sort
。然而
_RandomAccessIterator
毕竟只是一个名字,编译器不知道它表示哪些要求,更无法据此输出错误信息。

但是从C++20开始,编译器可以掌握这些信息了,不是通过

typename
后面的那个名字,而是由两个新关键词
concept
requires
支撑起来的。然后对于上面那个错误,编译器会说:“
std::random_access_iterator<std::list<int>::iterator>
不成立”(尽管目前我还没有体验过这种编译器)。

如果我们自己写的模板函数对类型有要求,可以在模板参数列表中写出:

#include <iterator>

template<std::random_access_iterator Iter>
void func(Iter _first, Iter _last)
{
// ...
}

那么

std::random_access_iterator
是如何实现的呢?

template<typename _Iter>
concept random_access_iterator = bidirectional_iterator<_Iter>
&& derived_from<__detail::__iter_concept<_Iter>,
random_access_iterator_tag>
&& totally_ordered<_Iter> && sized_sentinel_for<_Iter, _Iter>
&& requires(_Iter __i, const _Iter __j,
const iter_difference_t<_Iter> __n)
{
{ __i += __n } -> same_as<_Iter&>;
{ __j +  __n } -> same_as<_Iter>;
{ __n +  __j } -> same_as<_Iter>;
{ __i -= __n } -> same_as<_Iter&>;
{ __j -  __n } -> same_as<_Iter>;
{  __j[__n]  } -> same_as<iter_reference_t<_Iter>>;
};

意思看得懂,但不会写。别着急,这些语法我们一点点来讲。

 

requires关键词与需求

对模板参数的需求是嵌套的,深入到最底层,都是通过

requires
关键词实现的。“s”的存 15b0 在使代码在英语的语法中更加通顺一点。

requires
有两种用法:
requires
子句(requires-clause)和
requires
表达式。

requires表达式

requires
表达式产生一个
bool
值,语法为下列之一:

  • requires { 一系列requirements(需求) }

  • requires ( 参数列表 ) { 一系列requirements }

参数列表
用于创建一系列一定类型的变量,在requirements中使用。这些变量并不真实存在(只有语法功能),它们的作用域到后面的
}
为止。

Requirements有四种:简单需求(simple requirements)、类型需求(type requirements)、复合需求(compound requirements)和嵌套需求(nested requirements)。Requirements之间由分号分隔,只有当每个都满足时整个表达式才为

true

我们后面再来看

requires
表达式怎么用,现在我们要了解的是我们可以提出哪些需求。

简单需求

任意不以

requires
关键词开头的表达式都可以作为简单需求,当该表达式语法正确时需求满足。由于参数列表中的变量不实际存在,这个表达式当然也不会被求值。

requires (T a, T b)
{
a + b;
}

类型需求

typename
后跟一个类型名成为类型需求,当该类型存在时需求满足。类型需求可以用来检查嵌套类型和模板实例化。

requires
{
typename T::type;
typename S<T>;
}

复合需求

复合需求要求一个表达式合法,且结果类型符合一定约束,并可规定

noexcept

{ 表达式 } 可选的noexcept -> concept名 可选的<参数列表>;

后面会讲类型代入concept的规则,毕竟现在连concept都没讲呢。

requires (T x)
{
{++x} -> std::same_as<T&>;
}

嵌套需求与requires子句

嵌套需求就是

requires
子句(这句话不太严格,但没有必要纠结它们的区别)。
requires
后跟一个
bool
常量成为一个
requires
子句,仅当该
bool
常量的值为
true
时,子句所在的需求被满足,或所在的模板有效。预告一下,把参数代入一个concept可以得到
true
false
,而一个concept可以包含多个需求,所以嵌套需求就是多条已定义的需求的组合。

requires (T x) // requires表达式
{
requires true; // requires子句
requires std::random_access_iterator<T>; // requires子句,std::random_access_iterator是一个concept
requires requires (std::size_t n) // 第一个是requires子句,后跟bool值;第二个是requires表达式,产生bool值
{
x += n;
};
}

 

concept

我们一般用concepts(概念)一词指称这一套C++20特性。前面介绍了各种需求,它们写起来比较长,应该用一个名字来概括它,这个名字将成为一个

concept

concept
的语法很简单:

template<模板参数列表>
concept 名字 = bool表达式;

bool表达式
当然必须是常量表达式,通常是与模板参数列表有关的
requires
表达式,和其他
concept
的逻辑组合。
concept
可以产生
bool
值,想象一下把
concept
换成
bool
当变量模板就可以了。除此以外,
concept
作为
concept
可以用在
requires
子句和
requires
表达式中。我们稍后再来看其他用法。

concept
不能递归引用自己。
concept
不能单独声明,所以不会出现两个
concept
相互引用的情况。下一节将介绍的四种约束,
concept
一个都不能有。

标准库定义了许多

concept
,分布在
<concepts>
<iterator>
<ranges>
中。它们中的一些与
<type_traits>
is_
开头的类型有相同的含义,但名字不同(而且不是仅仅去掉
is_
)。

分类 名称 功能
语言核心 same_as 与某类型相同
derived_from 是某类型的子类
convertible_to 可以转换为某类型
common_reference_with 与某类型有
common_type
common_with 与某类型有
common_reference
integral 是整型
signed_integral 是带符号整型
unsigned_integral 是无符号整型
floating_point 是浮点类型
assignable_from 可从某类型赋值
swappable
swap
swappable_with 可与某类型
swap
destructible 可析构
constructible_from 可由某些类型的参数构造
default_initializable 可默认初始化
move_constructible 可移动构造
copy_constructible 可拷贝构造
比较 equality_comparable
==
比较
equality_comparable_with 可与某类型
==
比较
totally_ordered 可全序比较(
==
<
<=
等)
totally_ordered_with 可与某类型全序比较
对象属性 movable 可移动和
swap
copyable 可拷贝且
movable
semiregular 可默认构造且
copyable
regular
equality_comparable
&&
semiregular
可调用 invocable 可用某些类型的参数调用
regular_invocable
invocable
且无状态
predicate
bool
谓词
relation 是二元关系
equivalence_relation 是等价(
==
)关系
strict_weak_order 是严格弱序(
<
)关系

对于最后两个

concept
,除了有各种可调用的函数的需求以外,
==
运算符必须满足自反性与对称性,
<
运算符也类似。这些是句法上无法检查的,所以这两个
concept
更像是一种规约:如果模板参数被这种
concept
约束,那么客户调用时传入的参数就得满足这些语义需求。由于
concept
不能被特化,这一任务只能落到客户肩上,并且我不认为C++能进化出语义检查。

有些资料中的标准库

concept
是帕斯卡命名(PascalCase)的,因为最初的concept提案中是这样写的,原因可能是为了让它看起来属于新的C++20,或是与模板参数列表中类型大写的习惯一致。后来几个C++元老决定把
concept
换回C++标准命名法(Rename concepts to standard_case for C++20, while we still can),单词组成也略有修改。后来又有少许修改,以最新标准草稿(写作时为N4868)为准。

约束

现在到了应用

concept
的时候了。Constraint(约束)指定模板参数的需求,是以下需求的逻辑与:

  1. 模板参数前的concept;

    template<Concept T> // `Concept`是一个concept,下同
    void f(T);
  2. 模板参数列表后的

    requires
    子句;

    template<typename T>
    requires Concept<T>
    void f(T);
  3. 在简略函数模板声明(用

    auto
    替代模板类型,C++20特性)中,类型占位符(
    auto
    )前的concept;

    void f(Concept auto _arg);

    说来惭愧,写C++这么久,我从来没有过简写模板类型为

    auto
    的想法,明明是知道泛型lambda的。

  4. 在函数声明最后的

    requires
    子句。

    template<typename T>
    void f(T) requires Concept<T>;

这些requirements当然可以同时存在:

template<Concept1 T>
requires Concept2<T>
void f(T) requires Concept3<T>;

Concept2<T>
Concept3<T>
都在
requires
子句中,产生
true
false
,任意一个为
false
时该实例化无效。

但是如何理解

Concept1 T
呢?把
T
插到
Concept1
的参数列表的最前面,这里为空,所以就是
Concept1<T>
。另一个应用这一规则的地方是复合需求的返回类型部分,我们写
std::same_as<int>
,其含义为
requires std::same_as<T, int>
(但是不能这么写)。

如果模板参数代入时出现了不存在的类型或变量,该约束仅仅是不被满足,而不会产生编译错误。

约束可以用于函数模板、类模板和成员函数,非模板类的非模板成员函数除外。函数模板与类模板的约束是类似的,只有满足约束时模板才能实例化;对于成员函数的约束,如果它作用于模板类的模板参数,当约束不满足时,并不是类模板不能被实例化,而是实例化后的模板类没有这个成员函数:

#include <concepts>

template<std::regular T>
struct Container
{
template<std::same_as<int> U>
void f(U u) { }

void g()
requires std::same_as<T, int>
{ }
};

int main()
{
Container<int> ci;
ci.f(1);
ci.g();
Container<double> cd;
cd.f(1);
cd.g(); // error
}

像特化和偏特化一样,

concept
之间存在的包含关系也能用于重载决议——如果
A
成立则
B
一定成立,那么实例化时会优先匹配
B
的那一个实现。但是,
concept
的包含关系有时会不符合直觉,即两个
concept
看似包含却不能被编译器发现:

template<class T> constexpr bool is_meowable = true;
template<class T> constexpr bool is_cat = true;

template<class T>
concept Meowable = is_meowable<T>;

template<class T>
concept BadMeowableCat = is_meowable<T> && is_cat<T>;

template<class T>
concept GoodMeowableCat = Meowable<T> && is_cat<T>;

template<Meowable T>
void f1(T); // #1

template<BadMeowableCat T>
void f1(T); // #2

template<Meowable T>
void f2(T); // #3

template<GoodMeowableCat T>
void f2(T); // #4

void g(){
f1(0); // error, ambiguous:
// the is_meowable<T> in Meowable and BadMeowableCat forms distinct
// atomic constraints that are not identical (and so do not subsume each other)

f2(0); // OK, calls #4, more constrained than #3
// GoodMeowableCat got its is_meowable<T> from Meowable
}

如果

Meowable<T>
,那么一定有
is_meowable<T>
,所以
BadMeowableCat<T>
也满足,为什么不能判断出
Meowable
BadMeowableCat
之间的包含关系呢?包含关系作用在由
&&
||
连接的逻辑表达式上(实际上是合取与析取),通过深入到判断两个原子的(不是
&&
||
连接的)表达式是否相同从而决定包含关系,而只有相同的
concept
加上相同的模板参数才是相同,其他表达式即使再长得一样也是不同的。

在上面的例子中,编译器认为

BadMeowableCat
中的
is_meowable
Meowable
中的那个不一样,从而两个
concept之间没有包含关系,于是f1
的重载决议就是二义的;而
GoodMeowableCat
显然包含了
Meowable
,所以对
f2
的调用就是合法的。

另一方面,包含关系的检查一定会深入到最底层的

concept
,所以没有必要给所有自定义的
concept
进行非常严格的层次划分。但是有一点是原则性的,就是当你需要不同约束程度的
concept
时,它们的最底层必须都被有名字的
concept
封装起来。
<type_traits>
里有那么多变量模板,
<concepts>
还要分别用不同的、有些混淆性的名字包装一下,正是因为这个。

模板升级

面向过程、基于对象、面向对象、泛型和函数式这几个编程范式是逐渐加入C++的。起初,C++并没有模板,直到1990年。Bjarne Stroustrup对模板的要求是(以下翻译了跟没翻一样):

  • Full generality/expressiveness

  • Zero overhead compared to hand coding

  • Well-specified interfaces

后来的实现满足了前两条:针对第一条,C++模板是图灵完全的;针对第二条,C++模板带来更好的运行时性能(相比于

qsort
或虚函数这一类实现);唯独第三条没有解决,导致冗长的模板错误,并且衍生出以SFINAE为代表的一些奇技淫巧。它们贯穿我之前写的
<functional>
系列,成功劝退了很多读者。

C++20带来了解决方案——

concept
与约束。实际上
concept
早在零几年就出现在C++标准的草稿里了,但在2009年被删除,没有进入C++11(这一套工具非常复杂,C++20中只是它的简化版)。后来组委会又尝试了concepts lite,但也没有进入C++17。与此同时有一条支线concepts TS在发展,并在GCC中实现了出来,以此积累经验。C++20中的
concept
与TS还有一定区别,是总结了
concept
的各种实现以后选择的。

现在我们就来看一下

concept
如何给模板编程进行升级。以下例子来自
meds::function
,是我为一个华丽而无用的单片机项目写的库。

Tag Dispatching

首先是还讲点道理的tag dispatching。

S
是用来放对象的空间的类型,
T
是要放的对象的类型,一个
T
能否放进一个
S
将决定
initialize
等一系列操作的方法,而
object_manager
对外提供一个接口,在内部进行分类讨论:

template<typename S, typename T>
class object_manager
{
private:
using local_storage = std::integral_constant<bool,
std::is_trivially_copy_constructible<T>::value
&& sizeof(T) <= sizeof(S)
&& alignof(S) % alignof(T) == 0
>;

public:
static void initialize(S* _tar, T&& _obj)
{
initialize(_tar, std::move(_obj), local_storage());
}

private:
static void initialize(S* _tar, T&& _obj, std::true_type )
{
new (reinterpret_cast<T*>(_tar)) T(std::move(_obj));
}

static void initialize(S* _tar, T&& _obj, std::false_type)
{
_tar->template reinterpret_as<T*>() = new T(std::move(_obj));
}
};

T
可以放进
S
时,
local_storage
将成为
true_type
,匹配到第二个
initialize
,反之则为第三个。

这种操作还可以接受,但有了

concept
以后会更好:

template<typename S, typename T>
concept locally_storable = std::is_trivially_copy_constructible<T>::value
&& sizeof(T) <= sizeof(S)
&& alignof(
56c
S) % alignof(T) == 0;

template<typename S, typename T>
class object_manager
{
public:
static void initialize(S* _tar, T&& _obj)
{
reinterpret_cast<T*&>(*_tar) = new T(std::move(_obj));
}

static void initialize(S* _tar, T&& _obj) requires locally_storable<S, T>
{
new (reinterpret_cast<T*>(_tar)) T(std::move(_obj));
}
};

SFINAE

然后就是不讲章法的SFINAE了。下面我们要根据一个类的可比较性调用不同实现,分为两步:

function_eq_comp
中定义了
value
指示模板参数
T
类型的两个实例是否可以用
operator==
比较,
function_object_compare
根据其结果执行不同操作。

template<typename T>
class function_eq_comp
{
private:
using one = int;
struct two
{
one unused[2];
};

template <typename U,
typename = decltype(std::declval<U>() == std::declval<U>())>
static one test(int);
template <typename>
static two test(...);

public:
static constexpr bool value = sizeof(decltype(test<T>(0))) == sizeof(one);
};

template<typename T>
typename std::enable_if< function_eq_comp<const T&>::value, bool>::type
function_object_com
ad8
pare(const T& _lhs, const T& _rhs)
{
return _lhs == _rhs;
}

template<typename T>
typename std::enable_if<!function_eq_comp<const T&>::value, bool>::type
function_object_compare(const T& _lhs, const T& _rhs)
{
return false;
}

==
运算符可用时,
one test(int)
函数正确定义,
test
函数的返回类型将会是
one
value
true
,否则
one test(int)
错误,根据SFINAE,
test
的调用落入
two test(...)
value
false

当两个

const T&
不可比较时,
function_eq_comp<const T&>::value
false
std::enable_if
没有定义
type
,第一个
function_object_compare
的模板类型发生错误,根据SFINAE,该重载被忽略;与此同时第二个是可用的。反之,会调用到第一个。与tag dispatching中
true_type
false_type
并列出现类似,
function_eq_comp<const T&>::value
与它取
!
的表达式也都得出现,不能像上面的
concept
实现那样利用两个函数之间由重载优先级建立起的层次关系。与上一节相比,这里的代码重复更恶心一点。

concept
写会好看很多,尤其是在检查
operator==
可以用
std::equality_comparable
的前提下:

template<typename T>
bool function_object_compare(const T& _lhs, const T& _rhs)
{
return false;
}

template<typename T>
bool function_object_compare(const T& _lhs, const T& _rhs)
requires std::equality_comparable<const T&>
{
return _lhs == _rhs;
}

思考题

  1. 下面这段代码错在哪?

    template<typename T, typename U>
    requires (T t, U u) { t + u; }
    auto add(T t, U u)
    {
    return t + u;
    }
  2. * 查阅资料,写出一个嵌套需求接受但

    template
    requires
    子句不接受的表达式。(这道题没什么意义,只是想让你去查点资料。)

  3. 不查阅资料,判断

    std::derived_from
    的两个参数(基类、子类)哪个在前,并给出判断依据。

  4. 如何给一个函数添加约束,使得它能接受任意数量的相同类型的参数?

  5. 试用

    concept
    改写一个
    void_t
    技巧
    的实例。

扩展阅读

Constraints and concepts

C++20: Two Extremes and the Rescue with Concepts等一系列文章

Does constraint subsumption only apply to concepts?

The tightly-constrained design space of convenient syntaxes for generic programming

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: