ChatGPT解决这个技术问题 Extra ChatGPT

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

这个问题在这里已经有了答案:如何打印以逗号分隔的元素列表? (33 个回答) 使用 foreach 语法 [重复] (6 个回答) 6 年前关闭时,如何检查我是否在最后一个元素上。

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

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 构造?

这个

for(const auto& item : items) {
    cout << item;
} and_between {
    cout << separator;
}
如果您想从函数式编程书中获取一页,您始终可以使用 the accumulate method,尽管这在很多时候往往过于矫枉过正
C++ 没有算法join。这是一个巨大的耻辱。
@KevinW.,fold 不会帮助您-它将对每个元素应用操作,包括第一个和最后一个。
@SergeyA,如果您注意到,除了连接方法之外,accumulate 的 c++ 实现还接受 3 个参数,因此您只需从第二个元素连接到最后一个元素,并将第一个元素作为基本元素。这样就可以了。实际上,用分隔符分隔某些内容实际上是链接中的示例之一。
@SergeyA:我同意您不能在容器为空时调用 std::accumulate,但是一个元素应该没问题(单个元素作为 init 传递,最初是 begin == end,导致没有折叠)

J
Jarod42

我的方式(没有额外的分支)是:

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 的指针。
B
Ben Voigt

从迭代中排除一个结束元素是 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 有一个 XY 问题。但是 OP 提出的“除了最后一个”的问题很有趣且很有用,而这个答案提供了这一点。
@einpoklum:您可以提出一个很好的论点,即 ranged-for 通常更多地用于读取而不是更新......但是标准委员会选择允许对元素(而不是容器)的写访问并将其留给程序员选择只读访问(按值迭代变量或 const 引用)或写入(非 const 引用)。我的解决方案保留了该功能。
C
Community

你知道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
J
James Adkison

我不知道有什么特殊的成语。但是,我更喜欢先对特殊情况进行处理,然后再对其余项目执行操作。

#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:我正忙着写,看看我的答案。
M
Matteo Italia

通常我会以相反的方式进行操作:

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 照本宣科,在每次迭代时检查它(尽管在我的测试中注册)。
f
filipos

我喜欢简单的控制结构。

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

更进一步,这将在汇编中编写。
s
sudo rm -rf slash
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 

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


M
Mark Waterman

我不认为您可以在 somewhere 处使用特殊情况...例如,Boost 的 String Algorithms Library 有一个 join 算法。如果您查看它的 implementation,您会看到第一项的特殊情况(没有继续分隔符),并且在每个后续元素之前添加了 then 分隔符。


看看ostream joiners
@einpoklum 谢谢,我不知道那会来。强调没有算法(/迭代器)会绕过特殊情况:对于 ostream_joiner 它体现在布尔值中,用于跟踪您是否要编写第一个元素。基本论点是,当您以这种方式连接 N 个元素(其中 N > 1)时,您不会执行 N 次相同的操作——您有一个元素必须以不同方式处理。
好的,是的,但不是使用那些必须跟踪任何内容的木匠进行编码。另请参阅 @MaartenHilferink 的 answer。正如其他人在这里打趣的那样,“最好的代码行是您不必自己编写的代码行”的原则。
M
Maarten Hilferink

您可以定义一个函数 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


你真的可以不用第一个功能。幺半群的概念使您可以使用“左”的特殊空值和“右”的第一个实元素来开始。
p
pyj

我不知道“惯用语”,但 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 花哨工具给你一些东西。
J
JDługosz

我喜欢 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/ 迭代的枚举标记每个迭代项,然后在循环内进行切换。

要简单地省略最后一个元素,请使用范围过滤器作为 all-but-last。我不知道这是否已经在 Boost.Range 中,或者 Rangev3 正在提供什么,但这是使常规循环发挥作用并使其“整洁”的一般方法。


W
WeRelic

这是我喜欢使用的一个小技巧:

对于双向可迭代对象: 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 是双向可迭代的。
编辑以考虑单向迭代。您的问题没有将它们指定为约束,但值得考虑。第二个示例适用于两者。
在单向可迭代示例中,它不会以 O(n^2) 复杂度运行吗?我想 std::distance 除了一直迭代到结束之外没有办法知道答案,并且您在每次循环迭代中都在这样做。
@Jonathan True,这不是最有效的。我想可以通过在循环之前将可迭代的长度存储在 int 中并以某种方式将迭代器位置转换为 int 类型然后比较值来改进它。这会让它更接近 O(n),对吧? (对不起,如果我离开了,我不太擅长大 O 符号,这是我正在研究的东西)