【问题标题】:Should one prefer STL algorithms over hand-rolled loops?是否应该更喜欢 STL 算法而不是手动循环?
【发布时间】:2010-09-13 04:52:46
【问题描述】:

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

那么...应该优先使用 STL 算法而不是手动循环吗?

【问题讨论】:

    标签: c++ algorithm stl


    【解决方案1】:

    这取决于:

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

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

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

    【讨论】:

    • 有人可能会说 for (std::vector::iterator it = v.begin(); it != v.end(); ++it) {...}比 for_each(v.begin(), v.end(), ...) 可读性差。
    • 当然。如果它对您来说更具可读性,请使用 for_each。我的观点是,在函数范围之外创建一个 1-off 函子只是为了与 for_each 一起使用可能会导致问题。但如果函子已经存在,则使用 boost::lambda,或者您发现远程函子更具可读性,然后使用 for_each
    • 我同意凯文,我总是被现在不是实际循环本地的一次性函子推迟。听起来 lambda 东西会解决这个问题!
    【解决方案2】:

    我将在此违背常规,主张将 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 算法来编写更清晰、更清晰的代码,并开始在更细粒度的层面上进行思考。

    【讨论】:

    • 我同意你的观点,将实现细节放在其他地方会更好,但我也会在其中包含循环的逻辑。我宁愿有一个包含循环和实际 setFooCount 调用的 SetFooCounts(复数)函数。通过这种方法,它又回到了循环体中的首选位置。幸运的是,你在 2010 年发布了它,现在是 2017 年,C++ 有 lambda,所以我们都可以吃蛋糕。
    【解决方案3】:

    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

    【讨论】:

    • 同意。 BOOST_FOREACH 是 boost IMO 中最好的部分。让我更有效率
    • 我有一个项目,我无法访问 boost。所以我写了自己的宏,没那么酷,但仍然比为“header”编写整个宏要好,而且比std::for_each..
    【解决方案4】:

    这确实是 Scott Meyers 搞错的一件事。

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

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

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

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

    【讨论】:

      【解决方案5】:

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

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

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

      【讨论】:

        【解决方案6】:

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

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

        【讨论】:

          【解决方案7】:

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

          谢天谢地,在 C++0x 中,有了 lambda 函数、auto 和新的 for 语法,事情会变得轻松很多。在维基百科上查看C++0x Overview

          【讨论】:

            【解决方案8】:

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

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

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

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

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

            【讨论】:

            • 我不认为“喜欢”这个词暗示了一个硬性规定。 8v)
            【解决方案9】:

            我认为 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 还在每个步骤中引入了一个函数调用(可能通过尝试内联函数来缓解,但请记住“ inline" 只是对编译器的一个建议——它可能会忽略它)。

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

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

            【讨论】:

            • 有一些例外——排序、二分搜索、堆操作,也许还有其他一些——非常值得 IMO。但总的来说,我同意。
            • 旧评论,但正在浏览。您可以在 c++11 中使用:- auto weighted_sum = std::accumulate(std::begin(a_vector), std::end(a_vector),0,[i=0](auto sum, auto value) { i++; return sum + (i *value);});
            【解决方案10】:

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

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

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

            【讨论】:

              【解决方案11】:

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

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

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

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

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2018-07-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2010-12-30
                • 2011-05-10
                • 2010-09-13
                相关资源
                最近更新 更多