为什么我必须始终在 STL 的算法函数中显式指定范围,即使我想处理整个容器?

Why do I have to always specify the range in STL's algorithm functions explicitly, even if I want to work on the whole container?

本文关键字:范围 处理 STL 函数 算法 为什么      更新时间:2023-10-16

当使用STL的函数(如sort()min_element())时,我总是必须明确地通过begin和end指定范围:

void range_example()
{
    std::vector<int> list = {7, 3, 9, 1, 5, 2};
    auto found_element = std::min_element(list.begin(), list.end());
    std::cout << *found_element << std::endl;
}

如果我打算只在容器的一部分上工作,这是有意义的,但更多时候我需要函数在整个容器上工作。是否有一个重载函数不允许这样做的原因:

std::vector<int> list = {7, 3, 9, 1, 5, 2};
auto found_element = std::min_element(list);

是否有一种方法来完成我忽略的容器的总范围的函数调用?

EDIT:我知道我可以将其封装在一个函数中,但是因为这必须为所有函数完成,所以如果有更好的方法,我想避免这种情况。

大多数时候,标准库被设计为提供完成所有所需任务所需的最小接口,也就是说,它试图避免接口膨胀。当算法接受一对迭代器时,可以对整个容器进行操作,但如果算法接受容器,则不能对子范围进行操作。所以迭代器对是更基本的,这就是标准库提供的。方便函数通常不包括在内。

然而,你肯定不是第一个这样想的人,这是整个Boost。Range库致力于将范围(包括容器和任意范围)视为单个实体,而不是一对迭代器。

还有一个正式的建议,将Eric Niebler的range库合并到未来版本的c++标准库中。

这是因为STL算法与容器无关。迭代器为它们提供了一种统一的工作方式,唯一的限制是该算法对这些迭代器的保证是什么。

例如,如果你想对min_element()进行线性搜索,你只需要前向迭代器(即它们只需要支持operator++)。因此,您可以编写一个简单的模板实现,它基本上可以与每个容器一起工作,而不管容器在底层是如何实现的。

你可以重载函数,只接受容器,并在它们上应用begin()end(),但这意味着你要记住一个接口。

编辑

我想还有一些其他的论点可以提出。由于STL完全是关于数学之美,并强调算法与容器是分开的,所以总是传递迭代器会强化这一概念。

另一方面,就c++语言整体而言,Stroustrup的主要目标之一是教育开发人员。STL算法的全部功能来自于传递任意迭代器范围的能力,但大多数情况下,您希望对整个容器进行操作。如果您为整个容器提供了重载,可能会有人争论说,很多人永远不会费心去学习使用范围版本,因为正是这些版本会落入"另一个需要记住的接口"的类别。

容器或范围重载尚未完成的实际原因与概念提案有关。

现在,算法接受一堆模板参数,并对它们提出要求。如果您传递的类型不符合要求,则它们可能无法编译或无法正常工作。

重载几乎总是涉及不同数量的形参。

如果我们要添加容器/范围重载,那么我们要么必须给它们起一个新名字(ick),要么修改现有的算法使其重载智能。(iterator, iterator, value)重载和(range, value, function)重载具有相同数量的参数,调用哪一个很容易使编译器混淆(并且可能发生意外结果)。

虽然我们可以一个接一个地指定所有现有算法的重载约束,然后为范围添加重载,但此时代码和需求将是丑陋的。在概念被添加到语言之后,我们都希望有一组简明的概念来描述参数应该是什么,以及一个使实现简单明了的语言特性。

由于兼容性或其他原因,这些算法实际上可能不会对现有算法过载,但即使这样也会更容易解决。

最初,迭代器就足够了,它们将容器与算法解耦。当时可以添加范围,但是对容器进行清晰范围解释的语言机制有些缺乏(例如,decltype很有用),并且不是严格要求的。从那时起,就需要范围支持,但是要干净利落地做到这一点并不容易,而且(即将出现的)一种语言扩展将使它更干净、更容易。

您可以自己实现:

template<class Container>
typename Container::iterator min_element(Container& c) {
    using std::begin;
    using std::end;
    return std::min_element(begin(c), end(c));
}
std::vector<int> list = {7, 3, 9, 1, 5, 2};
auto found_element = min_element(list);
完整性:

template<class Container>
typename std::conditional<
  std::is_const<Container>::value,
  typename Container::const_iterator,
  typename Container::iterator>::type min_element(Container& c) {
    using std::begin;
    using std::end;
    return std::min_element(begin(c), end(c));
}

和支持数组:

template<typename T, size_t N>
T* min_element(T (&arr)[N]) { return std::min_element(arr, arr + N); }

这是我认为使用宏是好的时候之一。只要确保在宏内计算表达式没有副作用就可以了。

#include <boost/preprocessor/punctuation/comma.hpp>
// this is just: #define BOOST_PP_COMMA() ,
#define RANGE_ARGS( container ) container.begin ( ) BOOST_PP_COMMA() container.end ( )
#define RANGE_ARGS_C( container ) container.cbegin ( ) BOOST_PP_COMMA() container.cend ( )
#define RANGE_ARGS_R( container ) container.rbegin ( ) BOOST_PP_COMMA() container.rend ( )
#define RANGE_ARGS_CR( container ) container.crbegin ( ) BOOST_PP_COMMA() container.crend ( )

在您的例子中,这产生:

std::vector<int> list = {7, 3, 9, 1, 5, 2};
auto const found_element = std::min_element( RANGE_ARGS_C(list) );

自己定义这样的函数很容易。例如

#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
template <class T>
decltype( auto ) min_element( T &c )
{
    return std::min_element( std::begin( c ), std::end( c ) );
}
int main()
{
    int a[] = { 5, 7, 3, 1, 9, 6 };
    std::cout << *min_element( a ) << std::endl;
    std::vector<int> v( std::begin( a ), std::end( a ) );
    std::cout << *min_element( v ) << std::endl;
}    

程序输出为

1
1

我对算法std::sortstd::reverse做了这样的建议。你可以在我的个人论坛上读到它,我支持像我的个人网页。

虽然它是用俄语写的,但你可以用Bing或google翻译。