STL算法:为什么没有额外的容器接口(除了迭代器对之外)

STL algorithms: Why no additional interface for containers (additional to iterator pairs)?

本文关键字:接口 迭代器 为什么 算法 STL      更新时间:2023-10-16

我想知道为什么STL不重载它们的算法函数,这样我就可以通过简单地提供一个容器来调用它们,而不是采用更繁琐的方式来传递begin + end迭代器。我当然理解为什么我们还想使用迭代器对来处理容器/数组的子序列,但是,几乎所有对这些方法的调用都使用了整个容器:

std::for_each(myVector.begin(), myVector.end(), doSomething);

我觉得直接写

更方便,可读性和可维护性
std::for_each(myVector, doSomething);

是否有STL不提供这些重载的原因?[编辑:我不是说要用这个受限的接口来取代接口,而是要提供一个基于容器的接口!]它们会带来歧义吗?我在想这样的事情:

template<typename _Container, typename _Funct>
inline _Funct for_each(_Container c, _Funct f) {
    return for_each(begin(c), end(c), f);
}

我错过了什么吗?

它们确实为许多算法引入了歧义。很多<algorithm>看起来像

template<class iterator>
void do_something(iterator, iterator);
template<class iterator, class funct>
void do_something(iterator, iterator, funct);

如果添加额外的重载

template<class container, class funct>
void do_something(container, funct);

编译器在计算do_something(x, y)的含义时会遇到一些麻烦。*)

如果xy是同一个type,它将匹配iterator = typecontainer = type, funct = type c++ 11试图用能够识别容器和迭代器之间区别的"概念"来解决这个问题。然而,这些"概念"被证明太复杂而不能成为标准,所以这些重载也不能成为标准。

*)编译器对此不同意,Comeau编译器声称它有歧义,g++ 4.5和MSVC 10调用第一个函数


在评论中进行了长时间的讨论之后,这里有一个不像预期那样工作的例子——使用一个容器适配器,它也可以兼作谓词。

#include <iostream>
#include <vector>
template<class iterator>
void test(iterator, iterator)
{
   std::cout << "test iteratorn";
}
template<class iterator, class predicate>
void test(iterator, iterator, predicate)
{
   std::cout << "test iterator, predicaten";
}
template<class container, class predicate>
void test(const container& cont, predicate compare)
{
   std::cout << "test container, predicaten";
   test(cont.begin(), cont.end(), compare);
}
template<class container>
class adapter
{
public:
   typedef typename container::iterator   iterator;
   adapter(container* cont) : cont(cont)
   { }
   iterator begin() const
   { return cont->begin(); }
   iterator end() const
   { return cont->end(); }
   bool operator()(const iterator& one, const iterator& two)
   { return *one < *two; }
private:
   container* cont;
};
int main()
{
   std::vector<int>   v;
   adapter<std::vector<int>>   a(&v);
   test(a, a);
}
输出:

测试迭代器

http://ideone.com/wps2tZ

不幸的是,这是一个更普遍的问题;也就是说,迭代器的设计是为了击败那些蹩脚的C api和java风格的"将算法作为每个单独容器的方法"解决方案。它们是第一代通用解决方案,经过反思,它们不如我们花了20年时间思考后获得的其他可能的通用解决方案好,这并不奇怪。

添加这些容器重载只是解决问题空间的一小部分;它甚至可能使事情在未来变得更糟。解决方案是range, c++希望尽快引入。

要理解这一点,我认为必须理解c++算法的哲学。让我们先问这个问题:

为什么c++算法被实现为自由函数而不是成员函数?

答案很简单:避免实现爆炸。假设您有M容器和N算法,如果将它们实现为容器的成员,那么将会有M*N实现。这种方法有两个(相关的)问题:

  • 首先,它没有利用代码重用。大多数实现将被重复。
  • 第二,执行爆炸,这是上述的直接后果。

c++通过将它们实现为自由函数来解决这些问题,因此您只有N实现。操作容器的每个算法都接受一对迭代器,这对迭代器定义了范围。如果你想重载容器,而不是对迭代器,那么标准必须为每个算法提供这样的重载,并且将会有2*N实现,这几乎违背了c++最初将算法与容器分开的目的,并且这些函数中的一半不做任何另一半不能做的事情。

所以我不认为这是一个大问题。只是为了避免一个单个参数,为什么要实现N更多的函数(这也对其使用施加了一些限制,例如您不能将指针传递给它)?然而,如果程序员想要在他们的实用程序中使用这些函数,他们可以在任何时候基于标准算法实现它们!


你评论道:

2*N的实现实际上只有N个实现。其他N个是内联重载,它们直接调用算法的"真实"版本,所以它们是头文件。提供容器重载并不会破坏将算法与容器分离的目的,因为(正如你在我的例子中看到的)它们可以使用模板来处理所有类型的容器。

基于这个逻辑,我们可以很好地为M*N算法辩护。那么让它们也成为成员函数(并在内部调用自由函数)?我相信很多面向对象的人更喜欢

auto result = container.accumulate(val);

/

auto result = std::accumulate(container.begin(), container.end(), val);

这是Herb Sutter博客上的一个相关答案:为什么没有基于容器的算法?它展示了反例,就像Bo Persson在上面的回答中所做的那样。

有一个范围操作符库打算解决这个问题。冗长的内容被删减了好几次。

你的例子看起来像这样:

auto newVector = myVector * doSomething;

是的,doSomething -没有括号。

熟悉shell的习惯用法(使用std算法):

auto t = vector<int>{3,2,1,4} | sort | unique; 

应该指出的是,定义您自己的琐碎包装器来添加容器化版本是非常容易的。

例如:

template<typename Container, typename Func>
Func for_each(Container& c, Func f) {
    return std::for_each(c.begin(), c.end(), f);
}

现在你可以打你想打的电话了。没有歧义,因为您的包装器不在std名称空间中。可以定义带const Container&参数的重载。如果你想要调用c++ -11常量迭代器方法的版本(例如cbegin()),我认为你需要以不同的方式命名包装器。我使用for_each_const。

显然,正如其他用户提到的,这是一个棘手的问题,所以不幸的是,它已经很长时间了,在标准库中仍然没有解决方案。但是,已经有一些范围库可用,例如Boost:: range和Adobe源代码库中的一个,它们不仅提供了您在问题中描述的简单界面,而且还提供了一些更花哨的功能。

你的例子可以完美地使用Boost(我们在下面是using namespace boost::range::adaptors):

boost::for_each(myVector, doSomething);

我们还可以快速轻松地切片myVector:

boost::for_each(myVector | sliced(10, 20), doSomething)

我们甚至可以用另一个压缩myVector,通过谓词进行过滤,并在单个简单语句中对结果对的每个其他元素进行采样(这要求您在doSomethingElse中解压缩boost::combined产生的元组):

boost::for_each( boost::combined(myVector, myOtherVector) | strided(2), doSomethingElse)