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

时间:2008-09-25 18:37:13

标签: c++ algorithm stl

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

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

11 个答案:

答案 0 :(得分:19)

取决于:

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

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

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

答案 1 :(得分: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算法来制作更清晰,更清晰的代码,并开始在不太精细的层面上思考。

答案 2 :(得分: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

答案 3 :(得分:6)

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

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

但是如果你需要做的就是遍历一个集合并对每个项目做一些事情,那么只需执行正常的循环而不是尝试将代码分离到一个不同的函数中,这样就最终会将代码分成几个部分而不需要任何代码。真正的收获。

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

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

答案 4 :(得分:4)

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

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

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

答案 5 :(得分:3)

这取决于,如果算法不采用仿函数,则始终使用std算法版本。这对你来说既简单又清晰。

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

答案 6 :(得分:3)

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

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

答案 7 :(得分:2)

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

例如,我不会像

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

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

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

答案 8 :(得分: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构造,或者试图将其调整为累积调用?

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

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

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

答案 9 :(得分:1)

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

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

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

答案 10 :(得分:0)

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

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

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

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