从std::vector中打印逗号分隔的列表

6

我正在尝试打印一个由std::vector<MyClass>中的单个细节组成的逗号分隔列表。到目前为止,我看到的最简单和最聪明的方法是使用

std::ostringstream ss;
std::copy(vec.begin(), vec.end() - 1, std::ostream_iterator<std::string>(ss, ", "))
ss << vec.back();

当我打印字符串向量时,这个方法是有效的。然而,现在我想要打印 MyClass 的单个细节。我知道在Python中我可以做类似的事情:

(x.specific_detail for x in vec)

我希望能够得到一个与我感兴趣的内容相关的生成器表达式。我想知道在这里是否可以做类似的事情,或者我是否必须要这样做。

for (auto it = vec.begin(); it != vec.end(); ++it) {
    // Do stuff here
}

你考虑过使用基于范围的for循环吗? - wally
2
你可以使用std::transform来代替std::copy,只需使用lambda表达式即可。 - Fred Larson
2
@NickChapman Boost::join可以实现这个功能,还包括提供用户定义的谓词。 - PaulMcKenzie
2
我没有看到提到:http://en.cppreference.com/w/cpp/experimental/ostream_joiner - Jeff Garrett
1
@JeffGarrett 在生产环境中编写软件的人绝不会使用任何实验性或-std=c++1z的东西。这会在后面导致故障。Boost当然可以,但不完整的标准永远不行。 - Richard Hodges
显示剩余10条评论
7个回答

23

我看到的解决方法之一是:

std::string separator;
for (auto x : vec) {
  ss << separator << x.specific_detail;
  separator = ",";
}

2
这会浪费时间重新初始化分隔符变量。 - Interlooper
@Interlooper 即使编译器没有将其优化掉,我认为这是一个非常快的操作,不会破坏分支预测等。我怀疑性能将受到流式处理的主导。但是,是的,你在理论上是正确的,唯一确定的方法是进行测量。 - Chris Drew
同意。不过,你觉得检查迭代器是否在向量末尾的if语句比重新初始化变量更好吗? - Interlooper
1
一个 if 语句正是会破坏分支预测并且显著影响性能的东西,此外你还需要在每个循环中评估这个 if 操作。 - Chris Drew

3

一种相当简单且可重复使用的方法:

#include <vector>
#include <iostream>

template<class Stream, class T, class A>
Stream& printem(Stream&os, std::vector<T, A> const& v)
{
    auto emit = [&os, need_comma = false](T const& x) mutable
    {
        if (need_comma) os << ", ";
        os << x;
        need_comma = true;
    };

    for(T const& x : v) emit(x);
    return os;
}


int main()
{
    auto v = std::vector<int> { 1, 2, 3, 4 , 5 };

    printem(std::cout, v) << std::endl;
}

另一种定义可扩展协议以打印容器的方法:

#include <vector>
#include <iostream>

template<class Container>
struct container_printer;

// specialise for a class of container
template<class T, class A>
struct container_printer<std::vector<T, A>>
{
    using container_type = std::vector<T, A>;

    container_printer(container_type const& c) : c(c) {}

    std::ostream& operator()(std::ostream& os) const 
    {
        const char* sep = "";
        for (const T& x : c) {
            os << sep << x;
            sep = ", ";
        }
        return os;
    }

    friend std::ostream& operator<<(std::ostream& os, container_printer const& cp)
    {
        return cp(os);
    }

    container_type c;
};

template<class Container>
auto print_container(Container&& c)
{
    using container_type = typename std::decay<Container>::type;
    return container_printer<container_type>(c);
}


int main()
{
    auto v = std::vector<int> { 1, 2, 3, 4 , 5 };

    std::cout << print_container(v) << std::endl;
}

当然,我们可以进一步深入。

#include <vector>
#include <iostream>

template<class...Stuff>
struct container_printer;

// specialise for a class of container
template<class T, class A, class Separator, class Gap, class Prefix, class Postfix>
struct container_printer<std::vector<T, A>, Separator, Gap, Prefix, Postfix>
{
    using container_type = std::vector<T, A>;

    container_printer(container_type const& c, Separator sep, Gap gap, Prefix prefix, Postfix postfix) 
    : c(c)
    , separator(sep)
    , gap(gap)
    , prefix(prefix)
    , postfix(postfix) {}

    std::ostream& operator()(std::ostream& os) const 
    {
        Separator sep = gap;
        os << prefix;
        for (const T& x : c) {
            os << sep << x;
            sep = separator;
        }
        return os << gap << postfix; 
    }

    friend std::ostream& operator<<(std::ostream& os, container_printer const& cp)
    {
        return cp(os);
    }

    container_type c;
    Separator separator;
    Gap gap;
    Prefix prefix;
    Postfix postfix;
};

template<class Container, class Sep = char, class Gap = Sep, class Prefix = char, class Postfix = char>
auto print_container(Container&& c, Sep sep = ',', Gap gap = ' ', Prefix prefix = '[', Postfix postfix = ']')
{
    using container_type = typename std::decay<Container>::type;
    return container_printer<container_type, Sep, Gap, Prefix, Postfix>(c, sep, gap, prefix, postfix);
}


int main()
{
    auto v = std::vector<int> { 1, 2, 3, 4 , 5 };

    // json-style
    std::cout << print_container(v) << std::endl;

    // custom
    std::cout << print_container(v, " : ", " ", "(", ")") << std::endl;

    // custom
    std::cout << print_container(v, "-", "", ">>>", "<<<") << std::endl;

}

期望输出:

[ 1,2,3,4,5 ]
( 1 : 2 : 3 : 4 : 5 )
>>>1-2-3-4-5<<<

2

这里是一个简单的范围库:

template<class It>
struct range_t {
  It b, e;
  It begin() const { return b; }
  It end() const { return e; }
  bool empty() const { return begin()==end(); }
  std::size_t size() const { return std::distance( begin(), end() ); }
  range_t without_front( std::size_t n = 1 ) const {
    n = (std::min)(size(), n);
    return {std::next(b, n), e};
  }
  range_t without_back( std::size_t n = 1 ) const {
    n = (std::min)(size(), n);
    return {b, std::prev(e, n)};
  }
  range_t only_front( std::size_t n = 1 ) const {
    n = (std::min)(size(), n);
    return {b, std::next(b, n)};
  }
  range_t only_back( std::size_t n = 1 ) const {
    n = (std::min)(size(), n);
    return {std::prev(end(), n), end()};
  }
};
template<class It>
range_t<It> range(It s, It f) { return {s,f}; }
template<class C>
auto range(C&& c) {
  using std::begin; using std::end;
  return range( begin(c), end(c) );
}

现在我们已经准备好了。

auto r = range(vec);
for (auto& front: r.only_front()) {
  std::cout << front.x;
}
for (auto& rest: r.without_front()) {
  std::cout << "," << rest.x;
}

实时示例

现在您可以更加精细了。使用boost transform迭代器和boost range,您可以类似于Python中的列表推导式来完成某些操作。或者使用C++2a的Rangesv3库。

编写转换输入迭代器并不是非常困难,它只是一堆样板文件。只需查看输入迭代器的公理,编写一个类型来存储任意迭代器,并将大多数方法转发到它即可。

它还存储了一些函数。在*->上,对解引用的迭代器调用该函数。

template<class It, class F>
struct transform_iterator_t {
  using reference=std::result_of_t<F const&(typename std::iterator_traits<It>::reference)>;
  using value_type=reference;
  using difference_type=std::ptrdiff_t;
  using pointer=value_type*;
  using iterator_category=std::input_iterator_tag;

  using self=transform_iterator_t;
  It it;
  F f;
  friend bool operator!=( self const& lhs, self const& rhs ) {
    return lhs.it != rhs.it;
  }
  friend bool operator==( self const& lhs, self const& rhs ) {
    return !(lhs!=rhs);
  }
  self& operator++() {
    ++it;
    return *this;
  }
  self operator++(int) {
    auto r = *this;
    ++*this;
    return r;
  }
  reference operator*() const {
    return f(*it);
  }
  pointer operator->() const {
    // dangerous
    return std::addressof( **this );
  }
};

template<class F>
auto iterator_transformer( F&& f ) {
  return [f=std::forward<F>(f)](auto it){
    return transform_iterator_t<decltype(it), std::decay_t<decltype(f)>>{
      std::move(it), f
    };
  };
}

template<class F>
auto range_transfromer( F&& f ) {
  auto t = iterator_transformer(std::forward<F>(f));
  return [t=std::move(t)](auto&&...args){
    auto tmp = range( decltype(args)(args)... );
    return range( t(tmp.begin()), t(tmp.end()) );
  };
}
变压器的实时示例

如果我们添加--,我们甚至可以使用ostream迭代器

请注意,std::prev需要双向迭代器,这需要前向迭代器概念,这要求转换迭代器返回实际引用,这是很麻烦的。


2
以下是关于使用 std::transform 的示例:
#include <vector>
#include <string>
#include <iterator>
#include <algorithm>
#include <iostream>

int main()
{
    std::vector<std::string> strs = {"Testing", "One", "Two", "Three"};

    if (!strs.empty())
    {
        std::copy(std::begin(strs), std::prev(std::end(strs)), std::ostream_iterator<std::string>(std::cout, ", "));
        std::cout << strs.back();
    }
    std::cout << '\n';

    if (!strs.empty())
    {
        std::transform(std::begin(strs), std::prev(std::end(strs)), std::ostream_iterator<size_t>(std::cout, ", "),
                       [](const std::string& str) { return str.size(); });
        std::cout << strs.back().size();
    }
    std::cout << '\n';
}

输出:

Testing, One, Two, Three
7, 3, 3, 5

1
@todo 先检查向量是否为空。 - Richard Hodges
@RichardHodges:完成。 - Fred Larson

1

这是最终使用的内容

// assume std::vector<MyClass> vec
std::ostringstream ss;
std::for_each(vec.begin(), vec.end() - 1,
    [&ss] (MyClass &item) {
        ss << item.specific_detail << ", ";
    }
);
ss << vec.back().specific_detail;

@RichardHodges 这方面有检查措施。当它为空时,需要不同的行为。 - Nick Chapman

1
你可以使用已有的代码,只需更改传递给 std::ostream_iterator 的类型以限制其输出:
class MyClassDetail {
    const MyClass &m_cls;
public:
    MyClassDetail(const MyClass &src) : m_cls(src) {}
    friend std::ostream& operator<<(std::ostream &out, const MyClassDetail &in) {
        return out << in.m_cls.specific_detail;
    }
};

std::copy(vec.begin(), vec.end()-1, std::ostream_iterator<MyClassDetail>(ss, ", "));
ss << MyClassDetail(vec.back());

现场演示


-3
你可以简单地使用完全相同的代码,但定义一个<<运算符重载:
ostream &operator<<(ostream& out)
{
    out << m_detail;
}

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