用于“在每对连续的元素之间”进行迭代的成语 [重复]

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,但是一个元素应该没问题(单个元素最初被传递为initbegin == 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&amp; 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 

目标是使用&amp;&amp; 的评估方式。如果第一个条件为真,则评估第二个条件。如果不成立,则跳过第二个条件。

【讨论】:

【参考方案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::prevstd::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 符号,这是我正在研究的东西)

以上是关于用于“在每对连续的元素之间”进行迭代的成语 [重复]的主要内容,如果未能解决你的问题,请参考以下文章

Python“其他元素”成语[重复]

Rails 成语避免在 has_many 中重复:通过

特殊套管最后一个元素的最佳循环成语

C++中用于对象构造的**成语

迭代列表并将该值用于字典键[重复]

沉鱼落雁