基于范围的“ for”循环是否不赞成使用许多简单算法?


81

算法解决方案:

std::generate(numbers.begin(), numbers.end(), rand);

基于范围的for循环解决方案:

for (int& x : numbers) x = rand();

为什么我要std::generate在C ++ 11中使用比基于范围的for循环更详细的信息?


14
可组合性?哦,没关系,无论如何,带有迭代器的算法通常都是不可组合的……:(
R. Martinho Fernandes

2
...不是begin()end()
the_mandrill 2013年

6
@jrok我希望现在很多人range在他们的工具箱中都有一个功能。(即for(auto& x : range(first, last))
R. Martinho Fernandes

14
boost::generate(numbers, rand); // ♪
Xeo

5
@JamesBrock我们经常在C ++聊天室中对此进行讨论(它应该在脚本:P中的某个位置)。主要问题是算法通常返回一个迭代器,并接受两个迭代器。
R. Martinho Fernandes 2013年

Answers:


79

第一个版本

std::generate(numbers.begin(), numbers.end(), rand);

告诉我们您要生成一个值序列。

在第二版中,读者必须自己弄清楚这一点。

节省键入时间通常不是最佳选择,因为它通常会浪费在阅读时间上。多数代码读取的内容远远超过其键入的内容。


13
节省打字?知道了 为什么哦,为什么我们对“编译时完整性检查”和“敲击键盘上的键”有相同的称呼?:)
fredoverflow

25
节省打字通常不是最佳选择”。这与您正在使用的库有关。std :: generate很长,因为您必须numbers无缘无故地指定两次。因此:boost::range::generate(numbers, rand);。没有理由在完善的库中不能同时拥有更短和更清晰的代码。
Nicol Bolas 2013年

9
一切都在读者眼中。在大多数编程背景下,for循环版本都是可以理解的:将rand值放入collection的每个元素中。Std :: generate需要了解最新的C ++,或者猜测generate实际上意味着“修改项目”,而不是“返回生成的值”。
海德

2
如果您只想修改容器的一部分,那么可以std::generate(number.begin(), numbers.begin()+3, rand),不是吗?因此,我想number有时指定两次可能有用。
Marson Mao

7
@MarsonMao:如果只有两个参数std::generate(),则可以std::generate(slice(number.begin(), 3), rand)使用假设的范围切片语法std::generate(number[0:3], rand),甚至可以做得更好,甚至可以做得更好,例如这样可以消除重复的number同时仍然允许灵活指定范围的一部分。从三个论点开始做相反的工作std::generate()比较乏味。
Lie Ryan


30

我个人最初阅读以下内容:

std::generate(numbers.begin(), numbers.end(), rand);

是“我们正在分配范围内的所有内容。范围是numbers。分配的值是随机的”。

我的初步阅读:

for (int& x : numbers) x = rand();

是“我们正在对范围内的所有内容执行操作。范围是numbers。我们要做的是分配随机值。”

这些非常相似,但不完全相同。我可能想引起一读的一个可能的原因是,因为我认为有关此代码的最重要事实是它分配给该范围。所以有您的“我为什么要...”。我使用generateC ++std::generate是因为“范围分配”。就像btw一样std::copy,两者之间的区别就是您要从中进行分配。

不过,还有一些混杂因素。与基于numbers迭代器的算法相比,基于范围的for循环本质上更直接地表示范围为。这就是为什么人们使用基于范围的算法库的原因:boost::range::generate(numbers, rand);看起来比std::generate版本更好。

与此相反,int&在基于范围的for循环中会出现皱纹。如果范围的值类型不是int,那我们将在此做一些令人讨厌的微妙工作,这取决于它是否可以转换为int&,而generate代码仅取决于rand可分配给元素的返回。即使值类型是int,我仍然可能会停止考虑它是否是。因此auto,推迟了对类型的思考,直到我看到要分配的内容为止-auto &x我说“请引用range元素,无论可能具有什么类型”。早在C ++ 03,算法(因为它们是函数模板)都隐藏确切类型的方式,现在他们一个方式。

我认为,一直以来,最简单的算法仅比等效循环具有边际优势。基于范围的for循环可改善循环(主要是通过删除大部分样板,尽管它们要多得多)。因此,利润越来越紧,在某些特定情况下,您可能会改变主意。但是那里仍然存在风格差异。


您是否见过带有的用户定义类型operator int&()?:)
fredoverflow

@FredOverflow替换int&SomeClass&,现在您必须担心转换运算符和未标记的单参数构造函数explicit
TemplateRex

@FredOverflow:不要这样。这就是为什么如果它确实发生了,我将不会期望它,并且无论现在我多么偏执,如果我那时不去想它,它就会咬我;-)代理对象可以通过重载operator int&()operator int const &() const可以工作,但是同样可以通过重载operator int() const和来工作operator=(int)
史蒂夫·杰索普

1
@rhalbersma:我认为您不必担心构造函数,因为非const引用不会绑定到临时变量。它只是引用类型的转换运算符。
史蒂夫·杰索普

23

在我看来,有效的STL项目43:“首选算法调用优先于手写循环。” 仍然是一个很好的建议。

我通常编写包装函数来摆脱begin()/ end()hell。如果这样做,您的示例将如下所示:

my_util::generate(numbers, rand);

我认为它在传达意图和可读性方面都超过了for循环的范围。


话虽如此,我必须承认,在C ++ 98中,某些STL算法调用产生了无法言喻的代码,并且遵循“首选对手写循环的算法调用”似乎不是一个好主意。幸运的是,lambda改变了这一点。

考虑来自Herb Sutter的以下示例:Lambdas,Lambdas Everywhere

任务:在v中找到第一个元素是> x< y

没有lambda:

auto i = find_if( v.begin(), v.end(),
bind( logical_and<bool>(),
bind(greater<int>(), _1, x),
bind(less<int>(), _1, y) ) );

带lambda

auto i=find_if( v.begin(), v.end(), [=](int i) { return i > x && i < y; } );

1
与问题有点正交。只有第一句话解决了这个问题。
大卫·罗德里格斯(DavidRodríguez)-dribeas 2013年

@DavidRodríguez-dribeas是的。下半部分解释了为什么我认为第43项仍然是一个好的建议。
阿里

使用Boost.Lambda甚至比使用C ++ lambda函数还要好:auto i = find_if(v.begin(),v.end(),_1> x && _1 <y);
sdkljhdf hda

1
包装器+1。做同样的事情。从第一天(或也许第二天...)就应该成为标准
Macke

22

看来,人工循环,但可能会减少冗长,缺乏readabitly:

for (int& x : numbers) x = rand();

我不会使用此循环来初始化1数字定义的范围,因为当我看它时,似乎一个数字范围内进行迭代,但是实际上它并没有(本质上),即不是从范围读取,它正在写入范围。

使用时意图更加清晰std::generate

1.在这种情况下,初始化意味着为容器的元素赋予有意义的值。


5
但是,这不只是因为您不习惯基于范围的for循环吗?对我来说,很显然,该语句分配给范围中的每个元素。显然,generate会执行与您所熟悉的相同的事情std::generate,这可以由C ++程序员假定(如果他们不熟悉,他们会查找它,得到相同的结果)。
史蒂夫·杰索普

4
@SteveJessop:这个答案与其他两个没什么不同。它需要读者付出更多的努力,并且也更容易出错(如果忘记一个&字符该怎么办?)算法的优点是它们显示了意图,而对于循环则必须进行推断。如果循环的实现中存在错误,则不清楚这是错误还是故意的。
大卫·罗德里格斯(DavidRodríguez)-dribeas

1
@DavidRodríguez-dribeas:这个答案与其他两个IMO明显不同。它试图钻到原因是,笔者发现一个代码片段更清晰/比其他理解。其他人则对此进行了分析。这就是为什么我觉得这个有趣到足以回应它的原因:-)
史蒂夫·杰索普

1
@SteveJessop:您必须查看循环的主体才能得出结论,您实际上是在生成数字,但是对于std::generate,仅看一眼就可以说此函数正在生成某些内容。什么的东西是由第三个参数的函数回答。我认为这要好得多。
Nawaz 2013年

1
@SteveJessop:所以这意味着你属于少数。我会写对大多数人来说更清晰的代码:P。只是最后一个:我没有说其他人会像我一样阅读循环。我说(而意思),这是读这是误导我一分的方式,并且由于循环体是存在的,不同的程序员将不同的阅读弄清楚发生了什么那里。他们可能出于不同的原因而反对使用这种循环,根据他们的看法,所有这些可能都是正确的。
Nawaz 2013年

9

对于基于范围的循环,有些事情是您做不到的(简单地),而将迭代器作为输入的算法则可以做到。例如std::generate

使用一个发行版中的变量填充容器limit(不包括limit上的有效迭代器numbers),而使用另一个发行版中的变量填充容器。

std::generate(numbers.begin(), limit, rand1);
std::generate(limit, numbers.end(), rand2);

基于迭代器的算法可让您更好地控制要操作的范围。


8
尽管可读性原因是首选算法的巨大原因,但这是唯一的答案,表明基于范围的for循环只是算法的子集,因此无法弃用任何东西……
K-ballo 2013年

6

对于的特殊情况std::generate,我同意先前关于可读性/意图问题的答案。std :: generate在我看来似乎更清晰。但是我承认这在某种程度上是一种品味问题。

就是说,我还有另一个理由不放弃std :: algorithm-有些算法专门针对某些数据类型。

最简单的示例是std::fill。通用版本被实现为提供范围内的for循环,并且在实例化模板时将使用该版本。但不总是。例如,如果您提供一个范围std::vector<int>-通常它实际上会在后台调用memset,从而产生更快,更好的代码。

所以我想在这里打一个效率卡。

您的手写循环可能和std :: algorithm版本一样快,但是几乎不可能更快。不仅如此,std :: algorithm可能专门用于特定的容器和类型,并且是在干净的STL接口下完成的。


3

我的答案可能是,否。如果我们在谈论C ++ 11,那么也许(更像是没有)。例如std::for_each,即使与lambda一起使用也很烦人:

std::for_each(c.begin(), c.end(), [&](ExactTypeOfContainedValue& x)
{
    // do stuff with x
});

但是使用基于范围的for会更好:

for (auto& x : c)
{
    // do stuff with x
}

另一方面,如果我们谈论的是C ++ 1y,那么我认为不可以,基于范围的for不会淘汰这些算法。在C ++标准委员会中,有一个研究小组正在研究将范围添加到C ++的提案,并且还在处理多态lambda。范围将消除使用迭代器对的需要,而多态lambda将使您不必指定lambda的确切参数类型。这意味着std::for_each可以这样使用(不要把这个当成困难的事实,这就是今天的梦想):

std::for_each(c.range(), [](x)
{
    // do stuff with x
});

因此,在后一种情况下,该算法的优势在于,通过[]使用lambda进行写入,您可以指定零捕获?也就是说,与仅编写循环体相比,您已经从按词法出现的变量查找上下文中分离出了一部分代码。隔离通常对读者有帮助,而在阅读时却很少考虑。
史蒂夫·杰索普

1
捕获不是重点。关键是,使用多态lambda,您无需明确说明x的类型。
sdkljhdf hda

1
在那种情况下,在我看来,for_each即使是在使用lambda的情况下,在这种假设的C ++ 1y中仍然没有意义。foreach + captureing lambda目前是编写基于范围的for循环的详细方法,它变得比以前的循环略显冗长,但仍然比循环更复杂。for_each当然,这并不是说我不应该为自己辩护,但是即使在看到您的答案之前,我仍在思考,如果发问者想打败算法,他可能会被for_each选为所有目标中最软的;-)
史蒂夫·杰索普

不会捍卫for_each,但与基于范围的协议相比,它有一个微小的优势-您可以通过在其前面加上parallel_使其变得更容易使其并行化parallel_for_each(如果使用PPL,并假定这样做是线程安全的) 。:-D
sdkljhdf hda

@lego如果将s的实现隐藏在它们的接口后面并且可能会任意复杂(或任意优化),那么将您的“微小”优势确实是“巨大”优势std::algorithm
Christian Rau

1

应该注意的一件事是,一种算法表示完成的事情,而不是如何完成的事情。

基于范围的循环包括完成操作的方式:从第一个开始,应用并转到下一个元素,直到结束。即使是简单的算法也可以做不同的事情(至少对于特定容器有些重载,甚至不考虑可怕的向量),并且至少完成方法不是编写者事务。

对我来说,最大的不同是,尽可能地封装,并在可能的情况下使用算法证明句子的合理性。


1

基于范围的for循环就是这样。当然,直到标准被更改为止。

算法是一种功能。对参数有一些要求的函数。这些要求以标准的措辞表达,以允许利用所有可用执行线程的示例实现,并将自动加快您的速度。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.