是否应该优先选择STL算法而不是手动循环?

问题描述 投票:25回答:11

我似乎在问题和答案中看到比迭代器更多的“for”循环,而不是for_each(),transform()等。 Scott Meyers建议stl algorithms are preferred,或者至少他在2001年做过。当然,使用它们通常意味着将循环体移动到函数或函数对象中。有些人可能觉得这是一种不可接受的并发症,而其他人可能觉得这样可以更好地解决问题。

那么... STL算法应该优于手动循环吗?

c++ algorithm stl
11个回答
19
投票

这取决于:

  • 是否需要高性能
  • 循环的可读性
  • 算法是否复杂

如果循环不是瓶颈,并且算法很简单(如for_each),那么对于当前的C ++标准,我更喜欢手动循环以提高可读性。 (逻辑的局部性是关键。)

但是,现在C ++ 0x / C ++ 11得到了一些主要编译器的支持,我会说使用STL算法,因为它们现在允许使用lambda表达式 - 因此也就是逻辑的局部性。


1
投票

IMO,应该避免使用std :: for_each等许多标准库算法 - 主要是因为其他人提到的缺乏lambda问题,还因为存在不恰当的隐藏细节这样的问题。

当然,在函数和类中隐藏细节都是抽象的一部分,一般来说,库抽象比重新发明轮更好。但抽象的关键技能是知道何时做 - 以及什么时候不去做。过度抽象可能会损害可读性,可维护性等。良好的判断力来自经验,而不是来自不灵活的规则 - 当然,在学会打破规则之前必须学习规则。

OTOH,值得考虑的事实是很多程序员已经使用C ++(以及之前的C,Pascal等)很长一段时间。旧习惯很难改变,有一种叫做cognitive dissonance的东西经常导致借口和理性化。不过不要妄下结论 - 这至少与标准人员犯下决策后的不和谐一样可能。


0
投票

我认为一个重要因素是开发人员的舒适程度。

使用transform或for_each是正确的做法可能是正确的,但它并不是更有效,而且手写循环本身并不危险。如果开发人员编写一个简单的循环需要半个小时,而半天才能获得transform或for_each的语法,并将提供的代码移动到函数或函数对象中。然后其他开发人员需要知道发生了什么。

通过学习使用transform和for_each而不是手工循环,新的开发人员可能会得到最好的服务,因为他能够始终如一地使用它们而不会出错。对于我们其余的人而言,编写循环是第二天性,最好坚持我们所知道的,并在业余时间更熟悉算法。

这样说 - 如果我告诉我的老板我花了一天时间将手工制作的循环转换为for_each和转换电话,我怀疑他会非常高兴。


9
投票

我将在这里反对,并提倡使用STL算法和仿函数使代码更易于理解和维护,但你必须正确行事。你必须更加注意可读性和清晰度。特别是,你必须得到正确的命名。但是当你这样做时,你最终会得到更清晰,更清晰的代码,并且范式转变为更强大的编码技术。

我们来举个例子吧。这里我们有一组孩子,我们想把他们的“Foo Count”设置为某个值。标准的for循环迭代器方法是:

for (vector<Child>::iterator iter = children.begin();
    iter != children.end();
    ++iter)
{
    iter->setFooCount(n);
}

哪个,是的,它非常清楚,而且绝对不错的代码。只需稍微看一下就可以搞清楚。但是看看我们可以用适当的仿函数做些什么:

for_each(children.begin(), children.end(), SetFooCount(n));

哇,这正是我们所需要的。你不必弄清楚;你立即知道它正在设定每个孩子的“Foo计数”。 (如果我们不需要.begin()/ .end()废话,那就更清楚了,但你不能拥有所有东西,而且在制作STL时他们没有咨询我。)

当然,你需要定义这个神奇的仿函数SetFooCount,但它的定义很漂亮:

class SetFooCount
{
public:
    SetFooCount(int n) : fooCount(n) {}

    void operator () (Child& child)
    {
        child.setFooCount(fooCount);
    }

private:
    int fooCount;
};

总的来说它是更多的代码,你必须看另一个地方才能找到SetFooCount正在做的事情。但是因为我们很好地命名,99%的时候我们不必查看SetFooCount的代码。我们假设它完成它所说的,我们只需要查看for_each线。

我真正喜欢的是使用算法会导致范式转换。您不必将列表视为对象集合,而是对列表的每个元素执行操作,而是将列表视为第一类实体,并直接在列表本身上操作。 for循环遍历列表,在每个元素上调用成员函数来设置Foo Count。相反,我正在做一个命令,它设置列表中每个元素的Foo Count。它很微妙,但当你看到森林而不是树木时,你会获得更多的力量。

因此,通过一些思考和仔细的命名,我们可以使用STL算法来制作更清晰,更清晰的代码,并开始在更细粒度的层次上思考。


8
投票

几年前,std::foreach是一种让我诅咒STL的代码。

我不能说它是否更好,但我更喜欢在循环前导下使用我的循环代码。对我来说,这是一个强烈的要求。 std::foreach构造不允许我这样做(奇怪的是,就我而言,Java或C#的foreach版本很酷......所以我猜它确认了对我来说循环体的局部非常非常重要)。

因此,只有当只有一个可读/可理解的算法可用时,我才会使用foreach。如果没有,不,我不会。但这是一个品味问题,我想,因为我应该更加努力地去理解并学会解析所有这些......

请注意,加速的人显然感觉有点相同,因为他们写了BOOST_FOREACH:

#include <string>
#include <iostream>
#include <boost/foreach.hpp>

int main()
{
    std::string hello( "Hello, world!" );

    BOOST_FOREACH( char ch, hello )
    {
        std::cout << ch;
    }

    return 0;
}

见:http://www.boost.org/doc/libs/1_35_0/doc/html/foreach.html


6
投票

这真是Scott Meyers出错的一件事。

如果存在与您需要做的匹配的实际算法,那么当然使用该算法。

但是如果你需要做的就是遍历一个集合并对每个项目做一些事情,那么只需执行正常的循环而不是尝试将代码分离到另一个函子中,这样就最终将代码切割成比特而没有任何实际的好处。

还有一些其他的选项,比如boost :: bind或boost :: lambda,但那些是非常复杂的模板元编程的东西,它们在调试和单步执行代码时效果不佳所以通常应该避免使用它们。

正如其他人所提到的,当lambda表达式成为一等公民时,这一切都将改变。


4
投票

for循环是必不可少的,算法是声明性的。当你写std::max_element时,很明显你需要什么,当你使用循环来达到同样的目的时,它不一定如此。

算法也可以有轻微的性能优势。例如,当遍历std::deque时,专用算法可以避免冗余地检查给定的增量是否将指针移动到块边界上。

但是,复杂的函子表达式很快就会使算法调用无法读取。如果显式循环更易读,请使用它。如果可以在没有十层绑定表达式的情况下表达算法调用,则无论如何都更喜欢它。可读性比这里的性能更重要,因为这种优化是Knuth所着名的Hoare;一旦你意识到它是一个瓶颈,你将能够毫无困难地使用另一个构造。


3
投票

这取决于,如果算法没有采用仿函数,那么总是使用std算法版本。这对你来说既简单又清晰。

对于采用仿函数的算法,通常不会,直到可以使用C ++ 0x lambdas。如果算子很小并且算法很复杂(大多数不是),那么仍然可以使用std算法。


3
投票

我原则上是STL算法的忠实粉丝,但在实践中它太麻烦了。当你定义你的仿函数/谓词类时,两行for循环可以变成40多行代码,突然变得难以理解10倍。

值得庆幸的是,在使用lambda函数,auto和新的for语法的C ++ 0x中,事情会变得更容易。在维基百科上查看此C++0x Overview


2
投票

我不会使用严格的规则。有许多因素要考虑,比如经常在代码中执行某些操作,只是循环或“实际”算法,算法是否依赖于您必须传输到函数的大量上下文?

例如,我不会说类似的东西

for (int i = 0; i < some_vector.size(); i++)
    if (some_vector[i] == NULL) some_other_vector[i]++;

进入一个算法,因为它会导致更多的代码百分比明智,我将不得不处理以某种方式使算法知道some_other_vector。

还有很多其他的例子,使用STL算法很有意义,但你需要根据具体情况来决定。


2
投票

我认为STL算法接口是次优的并且应该避免,因为直接使用STL工具包(对于算法)可能会在性能上获得非常小的增益,但是当你''时,肯定会花费可读性,可维护性,甚至可写性。重新学习如何使用这些工具。

在向量上循环的标准有多高效:

int weighted_sum = 0;
for (int i = 0; i < a_vector.size(); ++i) {
  weighted_sum += (i + 1) * a_vector[i];  // Just writing something a little nontrivial.
}

比使用for_each构造,或试图将其纳入一个积累的调用?

您可能会认为迭代过程的效率较低,但是__每个步骤也引入了一个函数调用(这可能通过尝试内联函数来缓解,但请记住“内联”只是对编译器的建议 - 它可能会忽略它)。

无论如何,差异很小。根据我的经验,您编写的代码中超过90%不是性能关键,但编码时间至关重要。通过保持您的STL循环所有字面内联,它是非常可读的。对于自己或未来的维护者来说,旅行的间接性较小。如果它在你的风格指南中,那么你为你的程序员节省了一些学习时间(承认,学习如何正确使用STL第一次涉及一些问题)。最后一点是我的意思是可写性成本。

当然有一些特殊情况 - 例如,您可能实际上希望将for_each函数分开以在其他几个地方重复使用。或者,它可能是少数高性能关键部分之一。但这些都是特殊情况 - 例外而不是规则。

© www.soinside.com 2019 - 2024. All rights reserved.