使您自己的容器兼容C ++ 20范围

2021-03-23 05:30:24

在我的一些业余时间最近,我一直在享受一些新功能Inc ++ 20的学习。概念和与密切相关的条款是模板语法的两个伟大extrisions,可以消除我们曾经拥有的所有Sfinae垃圾的必要性,使我们的代码更加可读,更精确,提供更好的错误消息(尽管MSVC已经悲伤在这个写作时,在错误消息部门一直在滞后)。

另一个有趣的C ++ 20特征是添加范围库(也是范围算法),它为在对象的容器和序列上运行而提供了更合作的抽象。在大多数级别,范围内播放迭代器开始/结束对,但它比它更多。本质图并不是在范围内进行教程,但如果您想看到更多的内容,这是一个TalkTo Watch。

我今天要讨论的是将“范围兼容性”添加到您自己的ContainerClass的过程。许多C ++ Codebases我们在拥有自己的一组超出STLONES的容器类,出于各种原因 - 更好的性能,更多的Controlover存储器布局,更自定义的接口等。通过一点工作,您可以剪断您的自定义容器也用作范围,并与C ++ 20范围内互操作。该方法是怎么做的。

在高级别,有两种基本方法可以与范围进行交互。首先,它可以作为一个范围可读,这意味着我们可以迭代它,将其引入视图并传递ITTO范围算法,等等。在范围库的概率中,这被称为输入范围:可以为其他东西提供输入的范围。

另一个方向是接受从范围的输出,将输出存储到容器中.WE将稍后执行此操作。首先,让我们看看如何让您的容器充当输入范围。

我们必须制作的第一个决定是我们可以模拟的特殊输入范围。 C ++ 20StL定义了许多不同的范围概念,具体取决于其迭代器和其他事物的功能。其中几种在更加细微的范围内形成了一个层次的等级,以更严格的要求。一般来说,你的容器是为了实现它能够的最具体的范围概念。这使CodeThat能够与范围一起工作以进行更好的决策和使用更优化的代码路径。 (我们会在一分钟内看到这个的Someexamples。)

std :: ranges :: input_range:最赤裸的版本。它只需要您具有可以检索范围内容的迭代器。特别是,它不需要该范围可以迭代多次:迭代器不需要复制,并且开始/结束不需要多次向您提供迭代器。这可能是实际生成其内容的范围的适当概念,因为某些算法不容易/不可重复地或从网络连接或类似的数据接收数据。

std ::范围:: forward_range:范围可以像你喜欢的那样多次迭代,但只能在向前方向。例如,可以将迭代器复制并保存到以后从较早点恢复迭代。

std ::范围:: random_access_range:您可以在迭代器上有效地执行算术 - 您可以通过给定数量的步骤向前或向后抵消它们,或减去它们之间的步骤数。

std ::范围:: contiguous_range:该元素实际上存储为内存中的连续数组;迭代器基本上是花哨的指针(或字面上只是指针)。

除了输入范围概念的层次结构之外,还有几个其他独立的独立沃思:

std ::范围:: size_range:您可以有效地获得范围的大小,即,从开始结束的元素有多少。请注意,这是一个比randul_access_range更宽松的约束:后者要求您能够有效地测量范围内的任何一对迭代器之间的距离,而尺寸仅要求整个范围的大小是已知的。

std ::范围:: boRrageD_Range:表示范围不拥有其数据,即它引用(“借用”)在其他地方生活的数据。这可能是有用的,因为它允许引用/迭代器进入数据以超越范围对象本身的寿命。

所有这些概念都很重要的原因是,如果我正在编写在范围内运营的代码,我可能需要torequire一些这些概念,以便有效地完成工作。例如,一个排序例程将非常难以为少于random_access_range编写的(并且确实您将STD :: Ranges :: Sort所要求的)。在其他情况下,我可能能够更好地做事范围满足定张概念 - 例如,如果它是一个尺寸的_range,我可以为结果预先采用一些存储,而如果它只是一个input_range并且不再是,那么我必须动态地重新分配,因为我没有想到有多少元素是。

根据这些概念编写了其余的范围库(并且您可以编写您的OWSCODE,这些ode在使用这些概念的范围内经常运行)。因此,一旦容器的相关概念,它将自动被识别并用作范围!

在C ++ 20中,概念充当布尔表达式,因此您可以查看您的容器是否满足您期望的Choncepts,只需编写主题:

#include<范围> static_assert(std ::范围:: forward_range< mycoolcontainer< int>); // int只是一个任意选择的元素类型,因为我们//' t' t assert为不孤独的模板致意

除了通常的运行时测试之外,您还可以添加到您的测试套件 - 我很大,还有很大,还有很大,有利于为通用/成分造影造成的编译时刻表。

但是,当您第一次将其声明到您的代码时,它几乎肯定会失败。让我们看看你需要做些什么来实际满足范围概念。

开始和结束函数返回一些迭代器和Sentinel类型。 (我们将在一点点讨论这些。)

从input_range down to contip_range的每个概念都有一个相应的迭代器概念:std ::/pinn_iterator,std :: forward_iterator等。这是这些概念,其中包含定义不同类型范围的要求的真实性:它们列出了所有操作员的迭代器必须支持。

首先,有几个成员类型的别名别名,即任何迭代器类都需要定义:

第二个似乎是非常可理解的,但我真的不知道为什么差异_typerequirence是这里。在获取Torandom-Access迭代器之前,迭代器之间的差异无意义,实际上定义该操作。据我所知,更多常规迭代器的差异_TYPE实际上并不是用任何东西使用。尽管如此,根据C ++标准,必须在那里。似乎通常的成语是在这种情况下将其设置为std :: ptriff_t,但它可以是任何符号的整数类型。

(从技术上讲,您还可以通过专门用于您的迭代器的STD :: Iterator_traits来定义这些类型,但在这里我们只是将它们放在课堂上。)

迭代器必须是默认的初始化和可移动的。 (它不一定是可复制的。)

它必须与其哨兵相等(标记范围结束的值)相等。它不一定与其他迭代器相等。

它必须实施运算符++,在额先折叠和帖子段落位置。但是,PostIncrement版本不必返回任何内容。

它必须具有运算符*,可返回value_type的任何引用。

这里有一个兴趣点是默认初始化的要求意味着迭代器ClassCan不包含引用,例如,它来自它来自的容器的参考。但它可以存储指针。

模板< typename t> class迭代器{public:使用diversy_type = std :: ptrdiff_t;使用value_type = t;迭代器(); //默认初始化Bool运算符==(const sentinel&)const; // Sentinel T&amp的平等;运营商*()const; //取消转移迭代器&运算符++()//预递增{/ * do stuff ... * /返回*这; void运算符++(int)//后递增{++ *这;私人://实现...};

模板< typename t>类迭代器{public:// ...与前一个相同,除了:bool运算符==(const iterator&)const; //迭代器的平等void运算符++(int)//后递增{iterator temp = * this; ++ *这个;返回温度; }};

我不会详细介绍剩下的;您可以阅读CPPreference的详细信息。

一旦您的容器配备了满足相关概念的迭代器类,您将提供开始和结束功能以获取那些迭代器。有三种方法可以这样做:它们可以是容器上的成员函数,它们可以是生活在同一名称空间中的Thecontainer旁边的免费功能,或者他们可以是“隐藏的朋友”;他们只需要通过ADL找到。

从开始和结束的返回类型不必是相同的。在某些情况下,它可以使用方法返回不同类型的对象,一个“sentinel”,这不是一个迭代器; ITJust需要与迭代器相等,因此您可以告诉您何时已将容器结束。

这里值得一提的一个奇怪的是,如果你去了免费/朋友的函数路线,你将需要容器的const和non-const版本的toadd过载:

您可能认为只需提供const重载就足够了,但如果您这样做,只有Contain的Contapt的TheConst版本将被识别为范围!对于非Const容器来说,非Const重载必须存在展望。

奇怪的是,如果您将开始/结束作为成员函数,那么这未提出:Const重载将为两者都工作。

这种行为令人惊讶,而且我不确定它是否有意。然而,值得注意的是,据说骗局通常需要记住他们来自的容器的常规:ConstContainer应该给你一个“const迭代器”,它不允许突变其元素。因此,开始/结束的const和非const重载通常需要返回不同的壁器类型,因此您需要在任何情况下都有两个。 (如果是您的构建不可变容器,则会是例外;那么它只需要Const迭代器类型。)

除了开始和结束外,您还需要实现大小函数,如果适用.AGAIN,这可以是成员函数,免费功能或隐藏的朋友。此函数的主题满足STD ::范围:: size_range,该扫描范围算法(如前所述)算法更有效地运行。

所以,总结:允许您的自定义容器类作为范围可读,您需要:

确定您可以模型的范围概念,这主要是您可以提供的迭代功能级别的级别;

实现迭代师类(如果适用)符合所选迭代器概念的所有要求的迭代器类(如果适用);

完成此操作后,Ranges库应将您的容器识别为范围。它将通过范围算法接受它,我们可以拍摄它,我们可以迭代IT Itange-for Loops,等等。

如前所述,您可以通过断言您的容器是预期的范围概念来测试您正确地完成了所有事情。如果您正在使用GCC或Clang,如果您没有正确效力,这甚至会给您提供相当合理的错误消息! (在MSVC中,暂时,通过弹出敞篷并一次断言每个概念的子字核来缩小错误,以查看哪一个失败。)

我们讨论了如何使自定义容器用作C ++ 20范围库的输入。现在,随后将返回另一个方向:如何让您的容器捕获从威胁库的输出。

有几种不同的形式可以采取。一种方法是接受仿制函数的仿制函数(或其他方法,例如容器的其他方法,例如附加或插入方法)。例如,这允许容易地将其他容器(即也兼容的范围兼容)转换给容器。它还允许捕获范围的输出“流水线”(一系列视图将在一起)。

另一种形式的范围输出,它出现了某些范围算法,是通过输出迭代器,它是允许将值存储或插入occontainer的迭代器。

要编写拍摄泛型范围参数的构造函数(或其他方法),我们可以使用我们之前看到的SameRange概念。 C ++ 20中的一个简洁的新功能是用参数(或返回类型)的写入函数,约束以匹配给定概念。语法看起来像这样:

#include<范围> class mycoolcontainer {public:显式mycoolcontainer(std ::范围:: input_range auto&&范围){for(auto&&项目:范围){//处理项目}};

参数类型的语法概念名称自动提醒我们概念不是类型; intinis仍然在引擎盖下,一个执行参数类型扣除的模板函数(因此自动)。换句话说,以上是句法糖:

我更喜欢速记std ::范围:: input_range自动语法,但在这篇写作的时间仍然是摇晃(它应该在16.10中修复)。如果有疑问,useShe语法模板< std ::范围:: input_range r>。

在任何情况下,约束参数类型以满足input_range允许此构造overload在那里接受任何内容,它可以实现开始,结束和迭代器,正如我们在上一节中看到的那样。然后,您可以慷慨地迭代它,并与其做任何事情做任何事情。

范围参数被声明为Auto&&使其成为一个通用的参考,这意味着它可以接受百次数或rvalues;特别是,它可以接受返回范围的上方调用的结果,它可以接受管道的结果:

完全通用的范围接受方法可能不是最有用的东西。例如,如果我们已经存储int值的容器,我们将无法对串的acceptranges或其他任意类型进行大量意义。我们希望能够将一些额外的Constraintson提供范围的元素类型:也许我们只希望将可转换为int的元素类型。

有用的,范围库提供模板Range_Value_Tthat检索范围的元素类型 - 即,范围'siterator声明的value_type。有了这个,我们可以说明额外的约束:

您还可以选择需要一个更专业化的概念,如forward_range或random_access_range,如果您需要那些您正在执行的任何额外的功能。然而,只要容器通常应该实现最具体的范围概念,它可以难以实现它这需要一个范围参数,通常需要最普遍的rangeconcept它可以处理,或者它将过度限制可以传递什么样的范围。

也就是说,如果距离额外的要求,您可能会在其中切换到更有效的实现。例如,如果它是一个尺寸的_range,那么您可能在插入元素之前都可以重返存储。您可以使用constexpr:

显式MyCoolContainer(Input_Range_of< int> auto&&范围){如果constexpr(std ::范围:: size_range< decltype(范围)>){reaver(std ::范围:: size(范围) ; for(auto&&项目:范围){//处理项目}}

在这里,STD ::范围::尺寸是一个便利包,Wrapperthat知道如何调用范围的关联大小函数,无论它是否都是一种自由函数。

您也可以做的事情:检查范围是否是Contiguous_Range,并且该项目是某种目标可复制的,并且切换到Memcpy而不是迭代所有项目。

范围视图和管道在“拉动”模型上运行,其中管道由Proxyrange对象表示,当您迭代它时会产生懒惰的结果。将通用范围对象响应器到您的容器中是一种容易且有用的方法来消耗此类对象,并且可能足以大多数使用。但是,范围库中存在少数位,它在“推送”模型上运行,您可以通过输出器调用想要将值存储到容器中的函数。这提出了某些范围算法范围:: copy,范围:: transform和范围:: generate。

就个人而言,我看不到令人担忧的令人担忧的原因,因为它也是表达同一行动的淘汰意见;但为了完整起见,我将在这里讨论他们。

在这一点上,您将不会让您感到惊讶,就像有输入范围的概念一样,也有概念std ::范围:: output_range和std :: outcum_itorirator.in在这种情况下只有那个概念,不是一个他们的改进层次;但是,如果使用某些范围算法的定义,则会发现它们中的许多不实际使用output_iterator,但状态略有不同,较少或更具体的彼此的要求。 (这部分标准图书馆感觉比其他人完全烘烤一点;如果其中一些人在C ++ 23或更高版本的修订中被详细说明或抛光了更多的话,我不会惊讶。)

输出迭代器(广泛解释)的要求与输入终端的要求非常相似,只添加了解除解除迭代器返回的值必须可写入它:您必须能够执行* iter = foo;对于一些合适的Foo。如果youmplemented一个非const输入迭代器,它可能已经满足了这些要求。

也可以使用输出迭代器进行稍微异乎寻常的事物,例如返回接受分配的ProxyObject,并使用分配的值进行“某些东西”。这是STL的STD :: back_insert_iterator,它需要分配给它并将其附加到其容器(而不是覆盖容器中的Anexisting值)。 STL还有一些类似的东西,包括一个iteratorthat将字符写入Ostream。

“输入 - 输出”迭代器的范围算法中还有一些情况,例如重新开放的前进,如排序所在的范围。这些通常具有双向orrandom-access迭代器要求,加上需要取消引用的类型来交换,可移动和改变其他约束。这些细节可能不会与您有关,除非您做一些棘手的事情,如制作一个容器,就像在某种程度上发出一瞬间生成元素,或者返回代理对象而不是直接引用元素(如std :: vector< bool> bool> bool> )。

C ++ 20范围图书馆提供了许多强大,可商品的工具,用于操纵objects的序列,以及从最通用和抽象的容器形状的东西的一系列特异性倾向于非常具体,高效,实用。使用自己的容器类型时,它会很好能够利用这些工具。

正如我们所看到的那样,它几乎不是繁重的任务来实现您自己的容器的范围兼容性。大多数必要性是您已经已经的事情

......