迭代“在每个连续的元素对之间”之间的习语

79

每个人都会在某个时候遇到这个问题:

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++11构造实现相同的效果?


为了进一步扩展问题(超出这个问题的范围),我想说我也不想明确地设置第一个或最后一个元素的特殊情况。这是我不想被困扰的“实现细节”。那么,在想象中的未来的C ++中,应该像这样做:

for(const auto& item : items) {
    cout << item;
} and_between {
    cout << separator;
}

5
如果你想借鉴函数式编程的思想,你可以使用 累加算法(accumulate method)。但是这种方法有时候会过于复杂冗长,不一定适用于所有情况。 - Kevin W.
4
C++没有算法级别的“join”。这是非常遗憾的。 - SergeyA
1
@KevinW.,fold 不会帮助你 - 它将对每个元素应用操作,包括第一个和最后一个。 - SergeyA
3
如果你注意到的话,C++ 实现的 accumulate 函数除了连接方法之外还需要三个参数,因此你只需从第二个元素到最后一个元素进行连接,并将第一个元素作为基础元素即可。这种方法完全可以正常工作。事实上,使用分隔符分隔某些内容只是链接操作示例之一,具体请参考链接。 - Kevin W.
1
@SergeyA:我同意,如果你的容器为空,你不能调用std::accumulate,但一个元素应该没问题(这个单独的元素被作为init传递,而且最初begin == end,导致没有折叠)。 - Ben Voigt
显示剩余4条评论
12个回答

84

我的方法(不需要额外的分支)是:

const auto separator = "WhatYouWantHere";
const auto* sep = "";
for(const auto& item : items) {
    std::cout << sep << item;
    sep = separator;
}

这当然是更明显、更简单和更清洁的方式。 - edc65
简洁而优雅,这是我希望有一天能够写出的代码类型。 - pyj
4
好的,我会尽力进行翻译。请注意,我的回答将只包含翻译后的文本,不包括任何其他信息。是的,这很简单而且聪明。我可以想象下一次遇到这种任务时使用这种方法。但是,这样做的代价是引入一个额外的(可变的)变量,并在每次迭代时为其赋值。 - Frank Puffer
2
如果有人在上下文中使用此代码,其中 separator 需要是 std::string 而不是 char const*,那么直接移植此模式将导致每次迭代都产生昂贵的复制。可以通过以下方式避免这种情况:还需要一个空的常量 std::string,将 sep 指针指向该字符串,输出 *sep,最后在下一次迭代之前将 sep 指针指向 separator - underscore_d

26

把末尾元素排除在迭代外是Ranges提案旨在简化的操作。(请注意,解决字符串拼接等具体任务有更好的方法,将元素从迭代中剔除只会引入更多需要考虑的特殊情况,例如当集合已经为空时。)

在等待标准化的Ranges范式时,我们可以使用现有的范围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

对于与范围for循环一起使用的skip_last,需要使用双向迭代器;而对于类似的skip_first,仅需使用前向迭代器即可。


4
@SergeyA:当然这是有保障的,自从第一次C标准化以来就是这样。请参考**[conv.prom]**获取当前C++的措辞:“bool类型的prvalue可以转换为int类型的prvalue,false变为零,true变为一。” - Ben Voigt
6
OP实际上想要一个join操作,而不是跳过最后一个元素。(只需保留最后一个分隔符)。 - Jarod42
3
@SergeyA:当然,这并不是解决不带尾随分隔符的连接的唯一方法。 OP提出了一个X-Y问题。但是,OP所问的“除最后一个之外的每个”,是很有趣和有用的问题,而这个答案提供了这个功能。 - Ben Voigt
1
@SergeyA:这是XY问题。X是“在值之间用逗号写入”,Y是“提前停止循环一项”。您的评论表明您可以在没有Y的情况下完成X...但是Y对于其他问题仍然有用,而且这种技术值得分享。我的代码解决了所询问的问题,即“对于每个(范围for循环),除了最后一个”。 - Ben Voigt
2
@einpoklum:你可以提出一个很好的论点,即范围for循环通常用于读取而不是更新...然而,标准委员会选择允许对元素进行写访问(而不是容器),并由程序员选择只读访问(按值迭代变量或const引用)或写访问(非const引用)。我的解决方案保留了这个特性。 - Ben Voigt
显示剩余16条评论

25

你知道达夫设备(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;
}

(现场演示)

希望我没有毁了大家的一天。如果您也不想这样,请永远不要使用它。

(咕噜咕噜)对不起...


处理空容器(范围)的设备:

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

在线测试

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

3
不是使用foreach,而是对每个元素执行某个操作。大多数情况下,循环就是循环。 - Daniel Jour
7
谢谢。如果我没有知道这件事,我本可以过上长久而幸福的生活。 - Bilal Akil
2
我确实知道Duff的设备,但非常感谢您展示它在这里的应用。它是“更文明时代的优雅工具”(http://tvtropes.org/pmwiki/pmwiki.php/Main/ElegantWeaponForAMoreCivilizedAge)...好吧,也许是一个不太文明的时代。 - einpoklum
2
最有趣的事情是,布尔标志方法实际上被gcc优化成了设备 - SergeyA
5
一个简单的“goto”有什么不好? - Carsten S
显示剩余13条评论

13

我不知道针对这个问题是否有特别的成语。但是,我更喜欢将第一个情况单独处理,然后再对剩余的项执行操作。

#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"


我也有同样的反应;这也适用于任何能够头尾列表的语言。 - Tommy
6
需要注意的是,为了满足问题的要求,该循环可以写成 for( const auto& item : skip_first(values) ) - Ben Voigt
@einpoklum 是的,谢谢。我已经修复了拼写错误。 - James Adkison
1
@BenVoigt:skip_first是一个(假设存在的)我们需要定义的函数,还是有一个C++函数可以完成所需的操作? - R_Kapp
@R_Kapp:我忙着写它了,请看我的答案。 - Ben Voigt

13

通常我是反过来做的:

bool first=true;
for(const auto& item : items) {
    if(!first) cout<<separator;
    first = false;
    cout << item;
}

2
你需要一个if/else来让它工作。 - Ben Voigt
@BenVoigt,为什么?在这种情况下,即使没有else也可以完美地工作。但是,在每次迭代中它都有一个不愉快的分支 - 在极端情况下可能会影响性能(虽然我无法想象任何真实的情况)。 - SergeyA
@SergeyA:良好预测的分支基本上是免费的(而且经过几次迭代后,这个分支是完美预测的),我不会担心它。 - Matteo Italia
2
首先,它永远不会变成假。 - Christophe
2
@Deduplicator,我在Clang和gcc上进行了测试。 gcc完全删除了标志-只是命令jmp以确保它仅被调用一次,但clang按照书本的方法,在每次迭代中都进行了检查(在我的测试中放入寄存器)。 - SergeyA
显示剩余7条评论

7
我喜欢简单的控制结构。
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;
}

再进一步,这就得用汇编语言写了。 - Ruslan

6
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 

目标是利用 && 的计算方式。如果第一个条件为真,则计算第二个条件。如果不是真,则跳过第二个条件。


5

我认为你无法避免在某些地方使用特殊情况...例如,Boost的String Algorithms Library具有join算法。如果您查看其实现,您会发现第一项有一个特殊情况(没有前导分隔符),然后在每个后续元素之前添加了一个分隔符。


2
请查看 ostream joiners - einpoklum
@einpoklum 谢谢,我不知道那个要来了。这再次强调,没有算法(/迭代器)可以避免有一个特殊情况:对于 ostream_joiner,它体现在布尔值中,用于跟踪是否要写入第一个元素。基本论点是,当您以这种方式连接 N 个元素(其中 N> 1)时,您并不是进行相同的操作 N 次 - 您有一个必须以不同方式处理的元素。 - Mark Waterman
1
好的,是的,但使用这些连接器的编码不需要跟踪任何内容。还可以参考@MaartenHilferink的[答案](https://dev59.com/aVsW5IYBdhLWcg3wHUKb#35377869)。正如其他人在这里所说的:“最好的代码行就是你不必亲自编写的那一行”。 - einpoklum

5
你可以定义一个名为for_each_and_join的函数,该函数接受两个functor作为参数。第一个functor对每个元素进行操作,第二个functor对相邻元素对进行操作:
#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


你真的可以不用第一个函数。单子的概念让你可以从“左”边开始使用一个特殊的空值和第一个真实元素作为“右”边的起点。 - JDługosz

3
这是我喜欢使用的一个小技巧:
对于双向迭代对象: 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数量不变,你可能需要在这种情况下放弃范围基础迭代。
这个“技巧”只是将比较逻辑包装在一个单一的三元表达式中,如果你的比较逻辑相对简单的话。

我真的不知道诀窍在哪里。此外,您假设items是双向可迭代的。 - einpoklum
已编辑以考虑单向可迭代对象。虽然您的问题没有将它们指定为限制条件,但考虑到这一点是值得的。第二个示例将适用于两者。 - WeRelic
1
在单向可迭代对象的例子中,它不会以O(n^2)的复杂度运行吗?我想std::distance除了在每次循环迭代时都进行迭代到结尾的操作外,没有其他方法来知道答案。 - Jonathan
@Jonathan 是的,这并不是最有效率的。我想可以通过在循环前将可迭代对象的长度存储为 int 类型,并以某种方式将迭代器位置转换为 int 类型,然后再进行比较来改进它。那样应该能更接近 O(n),对吧?(如果我错了,很抱歉,我不太擅长大O符号,但我正在努力学习) - WeRelic

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接