为什么所有<algorithm>函数仅采用范围,而不采用容器?


49

中有许多有用的功能<algorithm>,但所有功能都对“序列”(一对迭代器)起作用。例如,如果我有一个容器并且喜欢std::accumulate在其上运行,则需要编写:

std::vector<int> myContainer = ...;
int sum = std::accumulate(myContainer.begin(), myContainer.end(), 0);

我打算做的是:

int sum = std::accumulate(myContainer, 0);

在我看来,这更具可读性和清晰度。

现在我可以看到,在某些情况下,您可能只希望对容器的某些部分进行操作,因此选择传递范围绝对对您有用。但是至少以我的经验来看,这是一种罕见的特殊情况。我通常要对整个容器进行操作。

可以很容易地编写一个包装函数,它接受一个容器,并呼吁begin()end()就可以了,但是这样的便利功能,不包含在标准库。

我想知道这种STL设计选择背后的原因。


7
STL通常提供便利包装,还是遵循旧的C ++的“脚踏实地”政策?
Kilian Foth,2014年

2
作为记录:与其编写自己的包装器,不如使用Boost.Range中的算法包装器;在这种情况下,boost::accumulate
ecatmur 2014年

Answers:


40

...可以选择通过范围绝对是有用的。但是至少以我的经验来看,这是一种罕见的特殊情况。我通常要对整个容器进行操作

您的经验中,这可能是一种罕见的特殊情况,但实际上,整个容器 都是特殊情况,而任意范围是一般情况。

您已经注意到可以使用当前接口来实现整个容器的情况,但是相反。

因此,库编写者可以选择是预先实现两个接口,还是只实现仍然涵盖所有情况的接口。


很容易编写一个包装函数,该函数接受一个容器并在其上调用begin()和end(),但是这些便利函数未包含在标准库中

诚然,特别是免费的功能std::beginstd::end现在包括在内。

因此,假设该库提供了便利的重载:

template <typename Container>
void sort(Container &c) {
  sort(begin(c), end(c));
}

现在,它还需要提供带有比较函子的等效重载,并且我们需要为所有其他算法提供等效项。

但是我们至少涵盖了要在一个完整的容器上运行的所有情况,对吗?好吧,不完全是。考虑

std::for_each(c.rbegin(), c.rend(), foo);

如果要对容器进行向后操作,则每个现有算法都需要另一个方法(或方法对)。


因此,基于范围的方法更简单:

  • 它可以完成整个容器版本可以做的所有事情
  • 整个容器的方法使所需的重载次数增加了一倍或三倍,而功能仍然不那么强大
  • 基于范围的算法也是可组合的(您可以堆栈或链接迭代器适配器,尽管这通常是在函数语言和Python中完成的)

当然,还有另一个正当的理由,那就是使STL标准化已经是很多工作,并且在被广泛使用之前用便利包装对其进行充实并不是在有限的委员会时间内大量使用。如果您有兴趣,可以在这里找到Stepanov&Lee的技术报告

如评论中所述,Boost.Range提供了一种更新的方法,而无需更改标准。


9
我认为没有人(包括OP)建议为每个特殊情况添加重载。即使“整个容器”比“任意范围”少见,但它肯定比“整个容器,颠倒”要普遍得多。将其限制为f(c.begin(), c.end(), ...),并且可能仅限制为最常用的重载(无论如何确定),以防止将重载数量加倍。而且,迭代器适配器是完全正交的(如您所注意到的,它们在Python中可以很好地工作,其迭代器的工作方式非常不同,并且没有您要谈论的大多数功能)。

3
我同意整个容器的转发情况很常见,但我想指出的是,它比可能的问题小得多的可能用途。特别是因为不是在整个容器和部分容器之间进行选择,而是在整个容器和部分容器之间进行选择,可能是相反的,也可能是其他方式。我认为这是公平的建议,认为使用适配器的复杂性更大,如果你也有改变你的算法超载。
没用的2014年

23
注意,如果STL提供了范围对象,则容器版本涵盖所有情况;例如std::sort(std::range(start, stop))

3
相反:可组合的功能算法(如map和filter)采用代表集合的单个对象并返回单个对象,它们当然不使用类似于一对迭代器的任何东西。
2013年

3
宏可以做到这一点:#define MAKE_RANGE(container) (container).begin(), (container).end()</ jk>
棘手怪胎2014年

21

原来,赫伯·萨特(Herb Sutter)对此发表一篇文章。基本上,问题是过载歧义。给定以下内容:

template<typename Iter>
void sort( Iter, Iter ); // 1

template<typename Iter, typename Pred>
void sort( Iter, Iter, Pred ); // 2

并添加以下内容:

template<typename Container>
void sort( Container& ); // 3

template<typename Container, typename Pred>
void sort( Container&, Pred ); // 4

难以区分41正确处理。

提出但最终未包含在C ++ 0x中的概念将解决该问题,并且也可以使用来规避它enable_if。对于某些算法,这完全没有问题。但是他们决定反对。

现在,在阅读完所有评论和答案之后,我认为range对象将是最佳解决方案。我想我看看Boost.Range


1
好吧,typename Iter对于严格的语言来说,仅使用a 似乎太鸭掌了。我宁愿如template<typename Container> void sort(typename Container::iterator, typename Container::iterator); // 1template<template<class> Container, typename T> void sort( Container<T>&, std::function<bool(const T&)> ); // 4等(这或许会解决模糊问题)
弗拉德

@Vlad:不幸的是,这不适用于普通的旧数组,因为没有T[]::iterator可用的数组。同样,适当的迭代器也不是任何集合的嵌套类型,只需定义即可std::iterator_traits
firegurafiku

@firegurafiku:好吧,通过一些基本的TMP技巧,数组很容易发生特殊情况。
弗拉德

11

基本上是一个遗留的决定。迭代器的概念是基于指针的,但是容器不是基于数组的。此外,由于数组很难传递(通常需要一个非类型的模板参数作为长度),所以一个函数通常只有可用的指针。

但是,是的,事后看来,这个决定是错误的。如果使用可以从begin/end或构造的范围对象,我们会更好begin/length。现在,我们有了多个_n后缀算法。


5

添加它们不会增加您的力量(您已经可以通过调用.begin().end()自己完成整个容器的操作),并且会向必须正确指定的库中添加另一件事,并由供应商添加,测试,维护,等等等

简而言之,它可能不存在,因为维护一组额外的模板只是为了避免整个容器的用户不必键入一个额外的函数调用参数,这是不值得的麻烦。


9
没关系,这是真的-但是最后,它也没有std::getline,而且仍然在库中。有人可能会说,扩展的控制结构并没有给我带来力量,因为我只能使用ifand 来做所有事情goto。是的,我知道这是不公平的比较;)我想我可以以某种方式理解规范/实现/维护的负担,但这只是我们在这里谈论的一个很小的包装,所以..
lethal-guitar

很小的包装程序无需花费任何代码,也许将其放入库中没有任何意义。
ebasconp 2014年

-1

到目前为止,http://en.wikipedia.org/wiki/C++11#Range-based_for_loop是一个不错的替代方法std::for_each。注意,没有显式的迭代器:

int a[5] = {1, 2, 3, 4, 5};
for (auto &i: a) { i *= 2; }

(灵感来自https://stackoverflow.com/a/694534/2097284。)


1
它仅解决的那一部分<algorithm>,而不是解决所有需要的实际算法beginend迭代器-但是收益不能被夸大!当我在2009ish中首次尝试使用C ++ 03时,由于循环的样板,我避开了迭代器,幸运的是,当时我的项目允许这样做。2014年在C ++ 11上重新启动,这是一次了不起的升级,C ++语言应该一直如此,现在我不能没有auto &it: them:)
underscore_d
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.