如何打印由逗号分隔的元素列表?

69

我知道如何在其他语言中实现,但不知道如何在C++中实现,而这里必须使用C++。

我有一组字符串(keywords),需要作为列表打印到out,字符串之间需要逗号分隔,但不能有尾随逗号。例如在Java中,我会使用StringBuilder构建字符串后,删除末尾的逗号。在C++中应该怎么做呢?

auto iter = keywords.begin();
for (iter; iter != keywords.end( ); iter++ )
{
    out << *iter << ", ";
}
out << endl;

我最初尝试插入以下代码块来完成它(将逗号打印移至此处):

if (iter++ != keywords.end())
    out << ", ";
iter--;

8
我知道使用for(auto iter = ...;可以让代码行数更少,但除非你明确打算在循环之后使用它,否则你应该将iter与循环的作用域绑定。 - user229044
@πάνταῥεῖ 虽然它们是重复的,但为什么闭包不是另一种方式?这篇文章肯定看起来更像是一个更好的重复目标。 - Passer By
36个回答

56

使用infix_iterator:

// infix_iterator.h 
// 
// Lifted from Jerry Coffin's 's prefix_ostream_iterator 
#if !defined(INFIX_ITERATOR_H_) 
#define  INFIX_ITERATOR_H_ 
#include <ostream> 
#include <iterator> 
template <class T, 
          class charT=char, 
          class traits=std::char_traits<charT> > 
class infix_ostream_iterator : 
    public std::iterator<std::output_iterator_tag,void,void,void,void> 
{ 
    std::basic_ostream<charT,traits> *os; 
    charT const* delimiter; 
    bool first_elem; 
public: 
    typedef charT char_type; 
    typedef traits traits_type; 
    typedef std::basic_ostream<charT,traits> ostream_type; 
    infix_ostream_iterator(ostream_type& s) 
        : os(&s),delimiter(0), first_elem(true) 
    {} 
    infix_ostream_iterator(ostream_type& s, charT const *d) 
        : os(&s),delimiter(d), first_elem(true) 
    {} 
    infix_ostream_iterator<T,charT,traits>& operator=(T const &item) 
    { 
        // Here's the only real change from ostream_iterator: 
        // Normally, the '*os << item;' would come before the 'if'. 
        if (!first_elem && delimiter != 0) 
            *os << delimiter; 
        *os << item; 
        first_elem = false; 
        return *this; 
    } 
    infix_ostream_iterator<T,charT,traits> &operator*() { 
        return *this; 
    } 
    infix_ostream_iterator<T,charT,traits> &operator++() { 
        return *this; 
    } 
    infix_ostream_iterator<T,charT,traits> &operator++(int) { 
        return *this; 
    } 
};     
#endif 

使用方法大致如下:

#include "infix_iterator.h"

// ...
std::copy(keywords.begin(), keywords.end(), infix_iterator(out, ","));

4
好的,就这样。为什么Boost库里面没有类似的东西? - Martin York
8
@Martin: 因为我从未费心提交它?我想起来了,我可能应该提交一下…… - Jerry Coffin
3
请提交它。 :) 你应该在邮件列表上发布,并询问是否有人需要类似的迭代器。 - GManNickG
2
@T.E.D.:在使用中,它的长度是您发布的代码长度的四分之一,而且它也更加通用(例如,如果您想要制表符分隔的输出,那么这完全不需要额外的工作)。简而言之,额外的长度主要是给出了一个如何完成工作的一般想法和一个已经基本完成并可供使用的代码之间的差异。 - Jerry Coffin
6
这种功能将在C++17中使用std::experimental::ostream_joiner实现,并且目前已经可以在GCC 6.0-SVN和Clang 3.9-SVN上使用(可在Wandbox上查看)。请参阅我的新回答。 - TemplateRex
显示剩余8条评论

43

在一个即将面世的实验性C++17编译器中,你可以使用std::experimental::ostream_joiner:

#include <algorithm>
#include <experimental/iterator>
#include <iostream>
#include <iterator>

int main()
{
    int i[] = {1, 2, 3, 4, 5};
    std::copy(std::begin(i),
              std::end(i),
              std::experimental::make_ostream_joiner(std::cout, ", "));
}

使用GCC 6.0 SVNClang 3.9 SVN的实时示例。


我喜欢这个。这很简洁明了。有没有一种方法可以应用变换而不使用std::transform,因为那需要创建中间存储? - kshenoy
1
这个是否通过了实验阶段?我在任何后续版本中都找不到 std::ostream_joiner - phuclv

31

因为每个人都决定使用while循环来做这件事,所以我将用for循环给出一个例子。

for (iter = keywords.begin(); iter != keywords.end(); iter++) {
  if (iter != keywords.begin()) cout << ", ";
  cout << *iter;
}

这是经典的容器打印循环。如果你厌倦了每次都写它,我们做了一个神奇的帮助头文件,可以为所有容器精确地完成这项工作。唯一的注释是,如果一切足够稳定并且您在循环后不需要迭代器,请将循环头更改为for (auto iter = keywords.begin(), end = keywords.end(); iter != end; ++iter) - Kerrek SB
3
或者,如果你真的想避免每次都进行比较,可以这样做:auto it = keywords.begin(); if (it != keywords.end()) cout << it++; 然后运行带有 cout << ", " << it; 的循环体。个人而言,我更喜欢将所有内容放在同一个位置。 - Kerrek SB
@Kerrek SB - ...或者你可以使用一个经过中间测试的循环。每当你发现自己编写一个带有“在第一次(或最后一次)迭代时也要执行此操作”的逻辑的循环时,你很有可能手头上有一个自然的经过中间测试的循环。 - T.E.D.
14
"middle-tested loop" 是什么意思? - Kerrek SB

24

假设输出流大致正常,因此向其写入空字符串确实不会有任何效果:

const char *padding = "";
for (auto iter = keywords.begin(); iter != keywords.end(); ++iter) {
    out << padding << *iter;
    padding = ", "
}

2
两年后再次查看这个问题,我更喜欢这种方法。聪明。 - T.E.D.

18

一种常见的方法是在循环之前先打印第一个项目,然后仅循环其余项目,在每个剩余项目之前预先打印逗号。

或者,您可以创建自己的流,以维护行的当前状态(endl 之前),并在适当的位置放置逗号。

编辑:您也可以使用由 T.E.D. 建议的中间测试循环。它将类似于:

if(!keywords.empty())
{
    auto iter = keywords.begin();
    while(true)
    {
        out << *iter;
        ++iter;
        if(iter == keywords.end())
        {
            break;
        }
        else
        {
            out << ", ";
        }
    }
}

我首先提到了“循环前先打印第一项”的方法,因为它可以使循环体保持简单,但任何一种方法都可以正常运行。


你的 if 检查中不需要 else 子句,因为 true 分支会将控制逻辑从循环中退出。我会更像这样写:if (iter == keywords.end()) break;。此外,如果你要在每个循环迭代中增加一些东西,我发现使用 for 循环并将迭代放在那里更容易阅读,人们已经习惯了看到它,并且知道你正在做什么。 - T.E.D.
1
当然,完成所有清理工作后,您基本上会得到下面的答案。它使用五行文本(如果我正确添加了empty()检查,则为六行),其中此占用17行。我认为这更容易理解。它将迭代放在标准位置,并消除了围绕产生逗号的代码的两个完整嵌套级别。 - T.E.D.
@T.E.D. 我没有使用for循环并在第三个语句中加入递增是因为那样行不通: 我需要先打印项目,然后再进行递增操作才能执行 end 测试。 - Mark B
@Sasha O 这就是为什么我必须在前面进行“非空”检查,以跳过特定情况下的其余工作。 - Mark B
@MarkB 您是正确的。很抱歉,不确定为什么会错过这个。 - Sasha O
显示剩余2条评论

14

有很多聪明的解决方案,但也有太多将代码弄得无法挽救而不让编译器发挥作用的方案。

显而易见的解决方案是特殊处理第一次迭代:

bool first = true;
for (auto const& e: sequence) {
   if (first) { first = false; } else { out << ", "; }
   out << e;
}

这是一种非常简单的模式,它:

  1. 不会扰乱循环: 一眼就能看出每个元素都会被迭代。
  2. 允许除了放置分隔符或者实际打印列表之外,else块和循环体中还可以包含任意语句。

这可能不是绝对最有效率的代码,但单个预测良好分支可能导致的性能损失很可能会被巨大的std::ostream::operator<<所掩盖。


这就是方法。 - Kartik Chugh

7
像这样吗?
while (iter != keywords.end())
{
 out << *iter;
 iter++;
 if (iter != keywords.end()) cout << ", ";
}

3
没有被踩,但是我对这个解决方案的问题在于它每次迭代都会对完全相同的条件进行两次检查。 - T.E.D.
测试相同的事情两次比每次迭代都针对beginend进行测试更好。如果编译器可以确定cout << ", "不会改变keywordsiter,它可以消除第二个测试。如果你真的想要DRY,那么使用if ( test ) break;do {} while ( test && cout << ", " );但这些通常被认为是不好的风格。 - Potatoswatter

5

我通常使用一种经过中间测试的循环方法来进行分隔符处理(在任何语言中)。C++代码如下:

for (;;) {
   std::cout << *iter;
   if (++iter == keywords.end()) break;
   std::cout << ",";
}

(注意:如果关键字可能为空,则需要在循环之前进行额外的if检查)
大多数其他解决方案每次循环迭代都会进行整个额外的测试。由于您正在进行I/O,因此花费的时间并不是一个巨大的问题,但这会冒犯我的感官。

@Potatoswatter - 我想这取决于你如何定义术语。对我来说,循环要么是从顶部测试、底部测试或中间测试。这个循环是中间测试的。至于实现它,在C语法的语言中,除非其中一种形式(while或do)恰好匹配特定情况,否则我通常更喜欢使用for()循环。不过这只是个人口味问题。 - T.E.D.
4
说到冒犯敏感性,你忽略了在开始时进行一次测试以确保 keywords.size() > 0 或等效的操作。这使得你的代码看起来比实际上简单。有点狡猾;-) - Steve Jessop
是的,抱歉,我可能在匆忙中看漏了那个注释。 - Steve Jessop
如果我理解正确的话,这段代码将在所有情况下取消引用 end - Mark B
1
@Mark B - 嗯...我明白了。准备按照你的顺序来做。从你的代码中,我可以看出你更喜欢将 iter++ 放在单独的一行上,所以请随意假装我已经这样做了。 :-) 其他排列方式也是可能的。 - T.E.D.
显示剩余3条评论

5
在Python中,我们只需要编写以下代码:
print ", ".join(keywords)

那为什么不呢:
template<class S, class V>
std::string
join(const S& sep, const V& v)
{
  std::ostringstream oss;
  if (!v.empty()) {
    typename V::const_iterator it = v.begin();
    oss << *it++;
    for (typename V::const_iterator e = v.end(); it != e; ++it)
      oss << sep << *it;
  }
  return oss.str();
}

然后就像这样使用它:
cout << join(", ", keywords) << endl;

与上面 Python 示例不同的是,在这个 C++ 示例中,分隔符和 keywords 可以是任何可流式化的内容,例如:
cout << join('\n', keywords) << endl;

自从C++11以来,“typename V::const_iterator”甚至可以用“auto”替代。 - Jarod42
我同意,但是我倾向于保持与C++11之前的标准兼容,因为我曾经被旧版Linux安装和旧版编译器困扰过太多次了... - Darko Veberic
我认为这个答案比大多数使用 for(...){ if(...){...}else{...} } 的答案要好,因为这个答案至少消除了循环中的跳转指令,感觉更加轻便、更好。 - r0n9

4

我建议您使用lambda表达式来帮助您简单地交换第一个字符。

std::function<std::string()> f = [&]() {f = [](){ return ","; }; return ""; };                  

for (auto &k : keywords)
    std::cout << f() << k;

这是 https://dev59.com/aVsW5IYBdhLWcg3wHUKb#35373017 的更糟糕的版本。 - Xeverous

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