27

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

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

4

11 回答 11

20

这取决于:

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

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

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

于 2008-09-25T18:42:23.077 回答
10

我将在此反对这种做法,主张将 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 Count”。(如果我们不需要 .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 算法来制作更清晰、更清晰的代码,并开始在更细粒度的层面上进行思考。

于 2010-01-29T23:46:55.160 回答
8

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

我不能说它是否更好,但我更喜欢将循环代码放在循环序言下。对我来说,这是一个强烈的要求。并且std::foreach构造不允许我这样做(奇怪的是,就我而言,Java 或 C# 的 foreach 版本很酷......所以我想它证实了循环体的位置对我来说非常非常重要)。

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

请注意,boost 的人显然也有同样的感觉,因为他们写了 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

于 2008-09-25T20:14:34.527 回答
6

这确实是斯科特迈耶斯弄错的一件事。

如果有一个实际的算法与您需要做的事情相匹配,那么当然可以使用该算法。

但是,如果您需要做的只是循环遍历一个集合并对每个项目执行某些操作,那么只需执行正常循环,而不是尝试将代码分离到不同的仿函数中,那最终只会将代码切成小块而没有任何实际收益。

还有一些其他选项,例如 boost::bind 或 boost::lambda,但这些都是非常复杂的模板元编程事物,它们在调试和单步执行代码时效果不佳,因此通常应避免使用它们。

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

于 2008-09-25T19:06:21.523 回答
4

循环是命令式的for,算法是声明式的。当您编写std::max_element时,很明显您需要什么,当您使用循环来实现相同时,不一定如此。

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

然而,复杂的函子表达式很快就会使算法调用变得不可读。如果显式循环更具可读性,请使用它。如果一个算法调用可以不用十层绑定表达式来表达,那么无论如何更喜欢它。在这里,可读性比性能更重要,因为这种优化是 Knuth 著名地归功于 Hoare 的原因。一旦你意识到它是一个瓶颈,你就可以毫无困难地使用另一个构造。

于 2008-09-25T20:13:29.380 回答
3

这取决于,如果算法不采用函子,那么总是使用 std 算法版本。你写起来更简单,更清晰。

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

于 2008-09-25T18:50:20.077 回答
3

原则上是 STL 算法的忠实拥护者,但实际上它太麻烦了。当你定义你的仿函数/谓词类时,两行 for 循环可以变成 40 多行代码,这突然变得难以弄清楚 10 倍。

值得庆幸的是,在 C++0x 中,有了 lambda 函数和新语法,事情会变得容易得多。在 Wikipedia 上查看此C++0x 概述。autofor

于 2008-09-26T01:01:01.887 回答
2

我不会为此使用硬性规定。有许多因素需要考虑,例如您经常在代码中执行某些操作,只是一个循环或“实际”算法,该算法是否取决于您必须传输到您的函数的大量上下文?

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

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

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

还有很多其他示例表明使用 STL 算法很有意义,但您需要根据具体情况做出决定。

于 2008-09-25T18:50:01.623 回答
2

我认为 STL 算法接口是次优的,应该避免使用,因为直接使用 STL 工具包(用于算法)可能会在性能上获得非常小的收益,但肯定会降低可读性、可维护性,甚至是一点可写性。重新学习如何使用这些工具。

一个标准的 for 循环在向量上的效率要高多少:

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 构造,还是尝试将其放入累积调用中?

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

无论如何,差异很小。根据我的经验,您编写的 90% 以上的代码不是性能关键,而是coder -time 关键。通过保持你的 STL 循环完全内联,它是非常可读的。对于您自己或未来的维护者来说,绊倒的间接性更少。如果它在您的风格指南中,那么您正在为编码人员节省一些学习时间(承认这一点,第一次学习正确使用 STL 涉及一些问题)。最后一点就是我所说的可写性成本。

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

于 2008-09-25T19:44:25.737 回答
2

IMO,应该避免使用许多标准库算法,例如 std::for_each - 主要是因为其他人提到的缺少 lambda 问题,但也因为存在不适当隐藏细节这样的事情。

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

OTOH,值得考虑的事实是,很多程序员已经使用 C++(以及在此之前,C、Pascal 等)很长时间了。旧习惯很难改掉,有一种叫做认知失调的东西,它经常导致借口和合理化。不过,不要草率下结论——标准的人至少有可能犯下决策后的不和谐。

于 2009-10-02T03:27:34.387 回答
0

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

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

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

这么说吧——如果我告诉我的老板我花了一天时间将手工循环转换为 for_each 并转换调用,我怀疑他会非常高兴。

于 2008-09-25T19:17:16.597 回答