基于范围的“for”循环是否弃用了许多简单的算法?

Does the range-based 'for' loop deprecate many simple algorithms?

提问人:fredoverflow 提问时间:1/10/2013 最后编辑:Peter Mortensenfredoverflow 更新时间:1/12/2013 访问量:8541

问:

算法解决方案:

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

基于量程的for环路解决方案:

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

为什么我要在 C++11 中使用更冗长的基于范围的 for 循环?std::generate

C++ 算法 STL C++11 foreach

评论

14赞 R. Martinho Fernandes 1/10/2013
可组合性?哦,没关系,无论如何,带有迭代器的算法通常都是不可组合的...... :(
2赞 the_mandrill 1/10/2013
...哪个不是和?begin()end()
6赞 R. Martinho Fernandes 1/10/2013
@jrok 我希望很多人现在在他们的工具箱中有一个功能。(即rangefor(auto& x : range(first, last)))
14赞 Xeo 1/10/2013
boost::generate(numbers, rand); // ♪
5赞 R. Martinho Fernandes 1/10/2013
@JamesBrock 我们经常在C++聊天室讨论这个问题(它应该在成绩单:P的某个地方)。主要问题是算法通常返回一个迭代器,并采用两个迭代器。

答:

42赞 David Rodríguez - dribeas 1/10/2013 #1

无论 for 循环是否基于范围,它都没有区别,它只是简化了括号内的代码。算法更清晰,因为它们显示了意图

22赞 Nawaz 1/10/2013 #2

在我看来,手动循环虽然可能会减少冗长,但缺乏可读性:

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

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

当您使用 时,意图会更加清晰。std::generate

1. 在此上下文中初始化意味着为容器的元素提供有意义的值。

评论

5赞 Steve Jessop 1/10/2013
不过,这不就是因为你不习惯基于范围的for循环吗?在我看来,这个语句分配给范围内的每个元素似乎很清楚。很明显,generate 执行与您熟悉的相同的操作,可以假设为 C++ 程序员(如果他们不熟悉,他们会查找它,相同的结果)。std::generate
4赞 David Rodríguez - dribeas 1/10/2013
@SteveJessop:这个答案与其他两个没有区别。它需要读者付出更多的努力,并且更容易出错(如果你忘记了一个字符怎么办?算法的优点是它们显示意图,而对于循环,你必须推断这一点。如果循环的实现中存在错误,则不清楚是错误还是故意的。&
1赞 Steve Jessop 1/10/2013
@DavidRodríguez-dribeas:这个答案与其他两个答案明显不同,IMO有很大不同。它试图深入研究作者发现一段代码比另一段代码更清晰/更易于理解的原因。其他人不加分析地陈述了这一点。这就是为什么我觉得这个足够有趣来回应它:-)
1赞 Nawaz 1/10/2013
@SteveJessop:你必须研究循环的主体才能得出结论,你实际上是在生成数字,但在这种情况下,仅仅通过一个观察,就可以说这个函数正在生成一些东西;该函数的第三个参数回答了什么。我认为这要好得多。std::generate
1赞 Nawaz 1/10/2013
@SteveJessop:所以这意味着你属于少数派。我会编写对大多数人来说更清晰:P代码。最后一点:我没有在任何地方说过其他人会以和我一样的方式阅读循环。我说(相当的意思是)这是读取循环的一种方式,这对我来说是误导性的,而且因为循环体在那里,不同的程序员会以不同的方式读取它以弄清楚那里发生了什么;他们可能会出于不同的原因反对使用这种循环,根据他们的看法,所有这些循环都是正确的。
79赞 Bo Persson 1/10/2013 #3

第一个版本

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

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

在第二个版本中,读者将不得不自己弄清楚。

节省打字通常是次优的,因为它最常在阅读时间上丢失。大多数代码的读取量比键入的要多得多。

评论

13赞 fredoverflow 1/11/2013
节省打字时间?哦,我明白了。为什么哦,为什么我们对“编译时健全性检查”和“敲击键盘上的键”有相同的术语?:)
26赞 Nicol Bolas 1/11/2013
"节省打字通常是次优的“胡说八道;这完全取决于您正在使用的库。std::generate 很长,因为您必须无缘无故地指定两次。因此:。没有理由不能在构建良好的库中同时拥有更短、更易读的代码。numbersboost::range::generate(numbers, rand);
9赞 hyde 1/11/2013
这一切都在读者的眼中。对于大多数编程背景来说,循环版本是可以理解的:将 rand 值放在集合的每个元素上。Std::generate 需要知道最近的 C++,或者猜测 generate 实际上意味着“修改项目”,而不是“返回生成的值”。
2赞 Marson Mao 1/11/2013
如果你只想修改容器的一部分,那么你可以,不是吗?所以我想有时指定两次可能会很有用。std::generate(number.begin(), numbers.begin()+3, rand)number
7赞 Lie Ryan 1/11/2013
@MarsonMao:如果你只有一个 两个参数 ,你可以用一个假设的范围切片语法来做甚至更好,这样可以消除重复,同时仍然允许灵活地指定部分范围。从三个参数开始做相反的事情更乏味。std::generate()std::generate(slice(number.begin(), 3), rand)std::generate(number[0:3], rand)numberstd::generate()
23赞 Ali 1/10/2013 #4

在我看来,有效的 STL 第 43 项:“更喜欢算法调用而不是手写循环”仍然是一个很好的建议。

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

my_util::generate(numbers, rand);

我相信它在传达意图和可读性方面都击败了基于范围的 for 循环。


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

请看以下来自 Herb Sutter 的示例:Lambdas, Lambdas Everywhere

任务:在 v 中找到第一个元素,即 和 。> x< y

不带 lambdas:

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; } );

评论

2赞 David Rodríguez - dribeas 1/10/2013
与问题有点正交。只有第一句话解决了这个问题。
0赞 Ali 1/10/2013
@DavidRodríguez-dribeas 是的。后半部分解释了为什么我认为第 43 项仍然是一个很好的建议。
0赞 sdkljhdf hda 1/10/2013
使用 Boost.Lambda,它甚至比C++ lambda 函数更好:auto i = find_if(v.begin(), v.end(), _1 > x && _1 < y);
1赞 Macke 2/4/2013
+1 用于包装器。做同样的事情。应该从第 1 天(或第 2 天)开始就符合标准......
0赞 463035818_is_not_an_ai 4/19/2023
如果用循环替换,第二部分几乎保持不变。它对 lambda 提出了观点,但对算法却没有意义std::find_if
3赞 sdkljhdf hda 1/10/2013 #5

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

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
});

评论

0赞 Steve Jessop 1/10/2013
因此,在后一种情况下,该算法的优点是,通过使用 lambda 写入,您可以指定 zero-capture?也就是说,与仅编写循环体相比,您已经将代码块与它在词法中出现的变量查找上下文隔离开来。隔离通常对读者有帮助,在阅读时较少考虑。[]
1赞 sdkljhdf hda 1/10/2013
捕获不是重点。关键是,使用多态 lambda 时,您不需要明确说明 x 的类型是什么。
1赞 Steve Jessop 1/10/2013
在这种情况下,在我看来,在这个假设的C++1y中,即使与lambda一起使用,它仍然毫无意义。Foreach+Capturing lambda 目前是一种编写基于范围的 for 循环的冗长方式,它变得不那么冗长,但仍然比循环更详细。当然,并不是说我认为你应该辩护,但即使在看到你的答案之前,我也在想,如果提问者想打败算法,他可以选择所有可能目标中最软的;-)for_eachfor_eachfor_each
0赞 sdkljhdf hda 1/10/2013
不会防守,但它与基于范围的 for 相比有一个微小的优势 - 你可以通过在它前面加上 parallel_ 来使它更容易并行(如果你使用 PPL,并假设它是线程安全的)。:-Dfor_eachparallel_for_each
0赞 Christian Rau 1/11/2013
@lego 如果将 S 的实现推广到以下事实,那么你的“微小”优势确实是一个“大”优势,即 s 的实现隐藏在它们的接口后面,并且可能是任意复杂(或任意优化)的。std::algorithm
30赞 Steve Jessop 1/10/2013 #6

就我个人而言,我对以下内容的初步阅读:

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

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

我的初步阅读:

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

是“我们正在对某个范围内的所有事情做一些事情。范围是 。我们所做的是分配一个随机值。numbers

这些非常相似,但并不完全相同。我可能想要挑起第一次阅读的一个合理原因是,因为我认为关于此代码的最重要的事实是它分配给范围。所以有你的“我为什么要......”。我使用是因为在C++中意味着“范围分配”。顺便说一句,两者之间的区别在于您从中分配的内容。generatestd::generatestd::copy

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

与此相反,在基于范围的 for 循环中是一个皱纹。如果范围的值类型不是 ,那么我们在这里做了一些令人讨厌的微妙的事情,这取决于它是否可转换为 ,而代码仅取决于可分配给元素的回报。即使值类型是 ,我仍然可能会停下来思考它是否是。因此,它推迟了对类型的思考,直到我看到分配了什么 - 我说“引用范围元素,无论它可能具有什么类型”。回到 C++03 年,算法(因为它们是函数模板)是隐藏确切类型的方法,现在它们是一种方式。int&intint&generaterandintautoauto &x

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

评论

0赞 fredoverflow 1/11/2013
你有没有见过一个带有 ?:)operator int&()
0赞 TemplateRex 1/11/2013
@FredOverflow替换为 和 现在,您不得不担心未标记的转换运算符和单参数构造函数。int&SomeClass&explicit
0赞 Steve Jessop 1/11/2013
@FredOverflow:别这么想。这就是为什么如果它真的发生了,我不会期待它,无论我现在对它多么偏执,如果我当时不碰巧想到它,它就会咬我;-)代理对象可以通过重载 和 来工作,但同样可以通过重载 和 来工作。operator int&()operator int const &() constoperator int() constoperator=(int)
1赞 Steve Jessop 1/11/2013
@rhalbersma:我认为你不必担心构造函数,因为非常量 ref 不会绑定到临时的。它只是引用类型的转换运算符。
9赞 Khaur 1/11/2013 #7

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

用一个分布中的变量填充容器(排除,是 上的有效迭代器),其余容器填充另一个分布中的变量。limitlimitnumbers

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

基于迭代器的算法使您可以更好地控制您正在操作的范围。

评论

8赞 K-ballo 1/11/2013
虽然可读性原因是一个巨大的偏爱算法的原因,但这是唯一的答案,表明基于范围的 for 循环只是算法的一个子集,因此不能弃用任何东西......
6赞 Sergei Nosov 1/11/2013 #8

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

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

最简单的例子是 。通用版本在提供的范围内作为 for 循环实现,在实例化模板时将使用此版本。但并非总是如此。例如,如果你给它提供一个范围,它通常会在引擎盖下调用,从而产生更快更好的代码。std::fillstd::vector<int>memset

所以我试图在这里打一张效率牌。

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

1赞 Cedric 1/11/2013 #9

应该注意的一件事是,算法表达的是做了什么,而不是如何做。

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

对我来说,这就是很大的区别,尽可能多地封装,并且尽可能地使用算法来证明句子的合理性。

1赞 Tomek 1/12/2013 #10

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

算法是一个函数。一个对其参数提出一些要求的函数。这些要求在标准中表述,例如,允许利用所有可用执行线程的实现,并自动加快速度。