与手动滚动循环相比,应该使用STL算法吗?

时间:2020-03-06 14:44:25  来源:igfitidea点击:

与for_each(),transform()等相比,我似乎在此处的问题和答案中看到了更多遍及迭代器的" for"循环。斯科特·迈耶斯(Scott Meyers)建议使用stl算法,或者至少在2001年他这样做。当然,使用它们通常意味着将循环体移动到函数或者函数对象中。有些人可能认为这是不可接受的并发症,而另一些人可能认为它可以更好地解决问题。

那么... STL算法应该比手动滚动的循环更受青睐吗?

解决方案

这取决于:

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

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

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

我不会为此使用硬性规定。有许多因素需要考虑,就像我们经常在代码中执行某些操作一样,只是一个循环或者"实际"算法,该算法是否取决于要传递给函数的大量上下文?

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

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

进入算法,因为这将导致更多的代码百分比,而且我将不得不处理以某种方式使算法知道some_other_vector的问题。

在许多其他示例中,使用STL算法很有意义,但是我们需要根据具体情况进行决策。

这取决于,如果算法不使用函子,则始终使用std算法版本。这对于我们而言既简单又清晰。

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

那真的是斯科特·迈耶斯(Scott Meyers)弄错的一件事。

如果有符合我们需要的实际算法,那么当然可以使用该算法。

但是,如果我们需要做的就是循环遍历一个集合并对每个项目做某事,则只需执行常规循环,而不是尝试将代码分离到不同的函子中,那样最终会将代码切成位,而没有任何实际收益。

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

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

我认为一个很大的因素是开发商的舒适度。

可以肯定的是,使用transform或者for_each是正确的做法,但这并没有更高的效率,而且手写循环并不是天生的危险。如果开发人员需要半小时才能编写一个简单的循环,而花半天时间才能正确获取transform或者for_each的语法,然后将提供的代码移至函数或者函数对象中,则花费了半个小时。然后其他开发人员将需要知道发生了什么。

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

这么说吧-如果我告诉老板我花了整整一天的时间将手工循环转换为for_each并转换呼叫,我怀疑他会不会很高兴。

我认为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构造,或者尝试使其适合于累加调用?

我们可能会认为迭代过程效率较低,但是for _每个函数还会在每个步骤中引入一个函数调用(可以通过尝试内联该函数来缓解这种情况,但是请记住,"内联"仅是对编译器的建议可能会忽略它)。

无论如何,差异很小。以我的经验,我们编写的代码中90%以上不是性能关键,而是编码时间。通过使STL循环保持原样的内联,它非常易读。对于我们自己或者将来的维护者而言,可以减少间接访问的次数。如果它在样式指南中,则可以为编码人员节省一些学习时间(承认这一点,第一次学习正确使用STL涉及一些陷阱)。这就是我所说的可写性成本。

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

for循环是必须的,算法是声明性的。当我们编写std :: max_element时,它很明显是我们所需要的,当我们使用循环来实现相同效果时,不一定如此。

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

但是,复杂的函子表达式很快使算法调用变得不可读。如果显式循环更易读,请使用它。如果可以在没有十层绑定表达式的情况下表示算法调用,则一定要使用它。在这里,可读性比性能更重要,因为这种优化正是Knuth著名地归因于Hoare的原因。一旦意识到它的瓶颈,我们就可以使用其他结构而不会遇到麻烦。

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

我不能说这是否更好,但是我更希望将循环代码放在循环序言下。对我来说,这是一个强烈的要求。而且,std :: foreach构造不允许我这样做(奇怪的是,就我而言,Java或者Care的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

我原则上是STL算法的忠实拥护者,但实际上这太麻烦了。当我们定义函子/谓词类时,两行for循环可能会变成40余行代码,突然变得难于理解10倍。

值得庆幸的是,在C ++ 0x中,具有lambda函数," auto"和新的" for"语法将使事情变得更加轻松。在Wikipedia上查看此C ++ 0x概述。

IMO,应避免使用诸如std :: for_each之类的许多标准库算法,主要是因为其他人提到的缺少lambda问题,而且还因为存在诸如隐藏细节之类的问题。

当然,在函数和类中隐藏细节是抽象的一部分,总的来说,库抽象比重新发明轮子要好。但是抽象的一项关键技能是知道何时执行和何时不执行。过多的抽象会损害可读性,可维护性等。好的判断取决于经验,而不是来自于僵化的规则,尽管我们当然必须在学习打破规则之前先学习这些规则。

OTOH,值得考虑的事实是,很长一段时间以来,许多程序员一直在使用C ++(在此之前,C,Pascal等)。旧习惯很难消亡,有一种叫做认知失调的东西,通常会导致借口和合理化。不要急于下结论,尽管标准专家至少有可能犯了决定后的不协调之罪。

我在这里与事实背道而驰,主张将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算法来编写更清晰明了的代码,并开始在较细粒度的层次上进行思考。