用于“在每对连续的元素之间”进行迭代的成语 [重复]
Posted
技术标签:
【中文标题】用于“在每对连续的元素之间”进行迭代的成语 [重复]【英文标题】:Idiom for iterating "between each consecutive pair of elements" [duplicate] 【发布时间】:2016-05-24 04:53:26 【问题描述】:每个人都会在某个时候遇到这个问题:
for(const auto& item : items)
cout << item << separator;
... 最后你会得到一个你不想要的额外分隔符。有时它不是打印,而是执行一些其他动作,但是相同类型的连续动作需要一些分隔符动作 - 但最后一个不需要。
现在,如果你使用老式的 for 循环和数组,你会这样做
for(int i = 0; i < num_items; i++)
cout << items[i];
if (i < num_items - 1) cout << separator;
(或者您可以将循环中的最后一项进行特殊处理。)如果您有任何允许非破坏性迭代器的东西,即使您不知道它的大小,您也可以这样做:
for(auto it = items.cbegin(); it != items.cend(); it++)
cout << *it;
if (std::next(it) != items.cend()) cout << separator;
我不喜欢最后两个的美学,喜欢范围广泛的 for 循环。我能否获得与最后两个相同的效果,但使用更漂亮的 C++11ish 构造?
为了进一步扩展问题(比方说,this one),我想说我也不想明确地将第一个或最后一个元素放在特殊情况下。这是我不想被打扰的“实施细节”。因此,在 imaginary-future-C++ 中,可能类似于:
for(const auto& item : items)
cout << item;
and_between
cout << separator;
【问题讨论】:
如果你想从函数式编程书中获取一页,你总是可以使用the accumulate method,尽管这在很多时候往往是矫枉过正 C++ 没有算法join
。这是一个巨大的耻辱。
@KevinW., fold
不会帮你 - 它会为每个元素应用操作,包括第一个和最后一个。
@SergeyA 如果你注意到,c++ 实现的累积除了加入方法外还接受 3 个参数,所以你只需从第二个元素加入到最后一个元素,并以第一个元素为基础元素。这样就可以了。事实上,用分隔符分隔某些内容实际上是链接中的示例之一。
@SergeyA:我同意当你的容器为空时你不能调用std::accumulate
,但是一个元素应该没问题(单个元素最初被传递为init
,begin == end
,导致没有折叠)
【参考方案1】:
我的方式(没有额外的分支)是:
const auto separator = "WhatYouWantHere";
const auto* sep = "";
for(const auto& item : items)
std::cout << sep << item;
sep = separator;
【讨论】:
这当然是更明显、更简单、更干净的方式 荒谬的简洁和优雅。我希望有一天能写出的代码类型。 是的,它既简单又聪明。我可以想象下次我有这种任务时使用这种方法。但是,它的代价是引入一个额外的(可变)变量并在每次迭代时为其分配一个值。 如果有人在separator
需要成为std::string
而不是char const*
的上下文中使用此模式,则直接移植此模式将在每次迭代中产生昂贵的复制。这可以通过以下方式避免:也有一个空常量std::string
,首先将sep
设为指向该常量的指针,输出*sep
,最后在下一次迭代之前将sep
设为指向separator
的指针。跨度>
【参考方案2】:
从迭代中排除结束元素是 Ranges 提案旨在简化的事情。 (请注意,有更好的方法来解决字符串连接的特定任务,将元素从迭代中分离出来只会产生更多需要担心的特殊情况,例如当集合已经为空时。)
当我们等待标准化的 Ranges 范式时,我们可以使用现有的 ranged-for 和一个小助手类来完成。
template<typename T> struct trim_last
T& inner;
friend auto begin( const trim_last& outer )
using std::begin;
return begin(outer.inner);
friend auto end( const trim_last& outer )
using std::end;
auto e = end(outer.inner); if(e != begin(outer)) --e; return e;
;
template<typename T> trim_last<T> skip_last( T& inner ) return inner ;
现在你可以写了
for(const auto& item : skip_last(items))
cout << item << separator;
演示:http://rextester.com/MFH77611
对于与 ranged-for 一起使用的 skip_last
,需要一个双向迭代器,对于类似的 skip_first
,拥有一个 Forward 迭代器就足够了。
【讨论】:
@SergeyA:表示零元素的输入列表创建零元素的输出,而不是中断。 @SergeyA:当然可以保证,自从 C 第一次标准化以来就一直如此。当前 C++ 措辞参见 [conv.prom]:“bool
类型的纯右值可以转换为 int
类型的纯右值,false
变为零,true
变为一个。”
OP 实际上想要一个 join
,而不是跳过最后一个元素。 (只应保留最后一个分隔符)。
@SergeyA:当然,这不是解决没有尾随分隔符的连接的唯一方法。 OP 有 X-Y 问题。但是 OP 提出的“除了最后一个之外的每个”的问题是有趣且有用的,而这个答案提供了这一点。
@einpoklum:您可以提出一个很好的论点,即 ranged-for 通常更多地用于读取而不是更新......但是标准委员会选择允许对元素的写访问(而不是对容器) 并留给程序员选择只读访问(按值迭代变量或 const 引用)或写入(非 const 引用)。我的解决方案保留了该功能。【参考方案3】:
你知道Duff's device吗?
int main()
int const items[] = 21, 42, 63;
int const * item = items;
int const * const end = items + sizeof(items) / sizeof(items[0]);
// the device:
switch (1)
case 0: do cout << ", ";
default: cout << *item; ++item; while (item != end);
cout << endl << "I'm so sorry" << endl;
return 0;
(Live)
希望我没有毁了每个人的一天。如果您不想这样做,那么永远不要使用它。
(咕哝)对不起……
处理空容器(范围)的设备:
template<typename Iterator, typename Fn1, typename Fn2>
void for_the_device(Iterator from, Iterator to, Fn1 always, Fn2 butFirst)
switch ((from == to) ? 1 : 2)
case 0:
do
butFirst(*from);
case 2:
always(*from); ++from;
while (from != to);
default: // reached directly when from == to
break;
Live test:
int main()
int const items[] = 21, 42, 63;
int const * const end = items + sizeof(items) / sizeof(items[0]);
for_the_device(items, end,
[](auto const & i) cout << i;,
[](auto const & i) cout << ", ";);
cout << endl << "I'm (still) so sorry" << endl;
// Now on an empty range
for_the_device(end, end,
[](auto const & i) cout << i;,
[](auto const & i) cout << ", ";);
cout << "Incredibly sorry." << endl;
return 0;
【讨论】:
不是foreach
,而是为每个元素做一些事情。大多数时候,一个循环就是一个循环。
我本可以在不知道这一点的情况下过上漫长而幸福的生活。谢谢。
我确实知道 Duff 的设备,但非常感谢您证明它适用于此。这是an elegant tool for a more civilized age...好吧,也许是一个不那么文明的时代。
最有趣的是,布尔标志方法被gcc有效地优化到设备中。
什么不是简单的goto
?【参考方案4】:
我不知道这有什么特殊的成语。但是,我更喜欢先特殊情况,然后对其余项目执行操作。
#include <iostream>
#include <vector>
int main()
std::vector<int> values = 1, 2, 3, 4, 5 ;
std::cout << "\"";
if (!values.empty())
std::cout << values[0];
for (size_t i = 1; i < values.size(); ++i)
std::cout << ", " << values[i];
std::cout << "\"\n";
return 0;
输出:"1, 2, 3, 4, 5"
【讨论】:
这里的反应完全一样;也非常适合任何可以列头尾列表的语言。 需要注意的是,该循环可以写成for( const auto& item : skip_first(values) )
...以满足问题的要求。
@einpoklum 是的,谢谢。我修正了错字。
@BenVoigt:skip_first
是我们必须定义的(假设的)函数,还是有一个 C++ 函数可以完成所需的工作?
@R_Kapp:我正忙着写,看我的回答。【参考方案5】:
通常我会采取相反的方式:
bool first=true;
for(const auto& item : items)
if(!first) cout<<separator;
first = false;
cout << item;
【讨论】:
你需要一个 if/else 来完成这项工作 @BenVoigt,为什么?在这种情况下,如果没有其他东西,它就可以很好地工作。然而,它在每次迭代中都有一个令人不快的分支——在极端情况下可能会影响性能(但无法想象任何真实的情况)。 @SergeyA:预测良好的分支基本上是免费的(经过几次迭代后,这个分支完全可以预测),我不会担心。 first 永远不会出错 @Deduplicator,我在 Clang 和 gcc 上进行了测试。 gcc 完全删除了该标志 - 只需命令 jmps 以确保它只被调用一次,但是 clang 照本宣科,在每次迭代时检查它(不过,在我的测试中注册)。【参考方案6】:我喜欢简单的控制结构。
if (first == last) return;
while (true)
std::cout << *first;
++first;
if (first == last) break;
std::cout << separator;
根据您的口味,您可以将增量和测试放在一行中:
...
while (true)
std::cout << *first;
if (++first == last) break;
std::cout << separator;
【讨论】:
更进一步,这将是用汇编编写的。【参考方案7】:int a[3] = 1,2,3;
int size = 3;
int i = 0;
do
std::cout << a[i];
while (++i < size && std::cout << ", ");
输出:
1, 2, 3
目标是使用&&
的评估方式。如果第一个条件为真,则评估第二个条件。如果不成立,则跳过第二个条件。
【讨论】:
【参考方案8】:我不认为您可以在某处遇到特殊情况...例如,Boost 的String Algorithms Library 有一个join 算法。如果您查看它的 implementation,您会看到第一项的特殊情况(没有继续分隔符),并且在每个后续元素之前添加了 then 分隔符。
【讨论】:
看看ostream joiners。 @einpoklum 谢谢,我不知道那会来。强调没有算法(/迭代器)会绕过特殊情况:对于 ostream_joiner 它体现在跟踪您是否要编写第一个元素的布尔值中。基本论点是,当您以这种方式连接 N 个元素(其中 N > 1)时,您不会执行 N 次相同的操作——您有一个元素必须以不同方式处理。 好的,是的,但不是使用那些木匠的编码必须跟踪任何东西。另见@MaartenHilferink 的answer。正如其他人在这里打趣的那样,“最好的代码行是您不必自己编写的代码行”的原则。【参考方案9】:您可以定义一个函数 for_each_and_join 以两个函子作为参数。第一个仿函数处理每个元素,第二个处理每对相邻元素:
#include <iostream>
#include <vector>
template <typename Iter, typename FEach, typename FJoin>
void for_each_and_join(Iter iter, Iter end, FEach&& feach, FJoin&& fjoin)
if (iter == end)
return;
while (true)
feach(*iter);
Iter curr = iter;
if (++iter == end)
return;
fjoin(*curr, *iter);
int main()
std::vector<int> values = 1, 2, 3, 4, 5 ;
for_each_and_join(values.begin(), values.end()
, [](auto v) std::cout << v;
, [](auto, auto) std::cout << ",";
);
现场示例:http://ideone.com/fR5S9H
【讨论】:
你真的可以不用第一个功能。 monoid 的概念让您可以使用“left”的特殊空值和“right”的第一个真实元素来开始。【参考方案10】:我不知道“惯用”,但 C++11 为双向迭代器提供了 std::prev
和 std::next
函数。
int main()
vector<int> items = 0, 1, 2, 3, 4;
string separator(",");
// Guard to prevent possible segfault on prev(items.cend())
if(items.size() > 0)
for(auto it = items.cbegin(); it != prev(items.cend()); it++)
cout << (*it) << separator;
cout << (*prev(items.cend()));
【讨论】:
我朋友的代码行太多了:-) ...我也不想检查items
的大小。
@einpoklum,我同意你的观点。 “最好的代码行是你不必编写的。”我只是想使用新的 C++11 精美工具为您提供一些东西。【参考方案11】:
我喜欢boost::join
功能。因此,对于更一般的行为,您需要一个为每对项目调用并且可以具有持久状态的函数。您可以将其用作带有 lambda 的函数调用:
foreachpair (range, [](auto left, auto right) whatever );
现在您可以使用range filters 回到基于范围的常规for
循环!
for (auto pair : collection|aspairs)
Do-something_with (pair.first);
在这个想法中,pair
被设置为原始集合的一对相邻元素。如果你有“abcde”,那么在第一次迭代中你会得到 first='a' 和 second='b';下次通过 first='b' 和 second='c';等等
您可以使用类似的过滤器方法来准备一个元组,该元组使用 /first/middle/last/ 迭代的枚举标记每个迭代项,然后在循环内进行切换。
要简单地省略最后一个元素,请使用范围过滤器来过滤所有元素。我不知道这是否已经在 Boost.Range 中,或者 Rangev3 正在提供什么,但这是使常规循环发挥作用并使其“整洁”的一般方法。
【讨论】:
【参考方案12】:这是我喜欢使用的一个小技巧:
对于双向可迭代对象:
for ( auto it = items.begin(); it != items.end(); it++ )
std::cout << *it << (it == items.end()-1 ? "" : sep); ;
使用三元 ?
运算符,我将迭代器的当前位置与 item.end()-1
调用进行比较。由于item.end()
返回的迭代器指的是在最后一个元素之后的位置,我们将其递减一次以获得我们实际的最后一个元素。
如果该项目不是可迭代的最后一个元素,我们返回我们的分隔符(在别处定义),或者如果它是最后一个元素,我们返回一个空字符串。
对于单向迭代(用 std::forward_list 测试):
for ( auto it = items.begin(); it != items.end(); it++ )
std::cout << *it << (std::distance( it, items.end() ) == 1 ? "" : sep); ;
在这里,我们使用当前迭代器位置和可迭代对象的结尾调用 std::distance 来替换之前的三元条件。
请注意,此版本适用于双向可迭代对象和单向可迭代对象。
编辑:
我知道你不喜欢 .begin()
和 .end()
类型的迭代,但如果你希望保持 LOC 倒计时,在这种情况下你可能不得不避开基于范围的迭代。
如果您的比较逻辑相对简单,则“技巧”只是将比较逻辑包装在一个三元表达式中。
【讨论】:
我真的不明白诀窍在哪里。此外,您假设项目是双向可迭代的。 已编辑以考虑单向迭代。您的问题没有将它们指定为约束,但值得考虑。第二个示例适用于两者。 在单向可迭代示例中,它不会以 O(n^2) 复杂度运行吗?我想std::distance
除了一直迭代到结束之外没有办法知道答案,而你在每次循环迭代中都在这样做。
@Jonathan True,这不是最有效的。我想可以通过在循环之前将可迭代的长度存储在 int 中并以某种方式将迭代器位置转换为 int 类型然后比较值来改进它。这会让它更接近 O(n),对吧? (对不起,如果我离开了,我不太擅长大 O 符号,这是我正在研究的东西)以上是关于用于“在每对连续的元素之间”进行迭代的成语 [重复]的主要内容,如果未能解决你的问题,请参考以下文章