6

I'm trying to print a comma separated list of a single detail from a std::vector<MyClass>. So far the simplest and cleverest way I have seen to do this is to use

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

That worked fine when I was printing a vector of strings. However, now I am trying to print a single detail about MyClass. I know in Python I could do something like

(x.specific_detail for x in vec)

to get a generator expression for the thing that I am interested in. I'm wondering if I can do something similar here or if I am stuck doing

for (auto it = vec.begin(); it != vec.end(); ++it) {
    // Do stuff here
}
Nick Chapman
  • 4,402
  • 1
  • 27
  • 41
  • Have you considered a [range based for loop](http://en.cppreference.com/w/cpp/language/range-for)? – wally Sep 18 '17 at 14:23
  • 2
    You could use [`std::transform`](http://en.cppreference.com/w/cpp/algorithm/transform) very much like you use `std::copy`, but with a lambda. – Fred Larson Sep 18 '17 at 14:24
  • @rex Using a range based for you end up with some pretty awful hacks to prevent a trailing comma – Nick Chapman Sep 18 '17 at 14:25
  • @NickChapman True, but how was the Python example different in that regard? Or the for loop example that you provide in the question for that matter? – wally Sep 18 '17 at 14:27
  • @FredLarson you need to prevent the trailing comma. – Nick Chapman Sep 18 '17 at 14:30
  • @rex in Python I could easily slice things to get what I wanted. Also in the for loop example there will also be hacky junk. That's why I'm hoping a lambda can save the day here. – Nick Chapman Sep 18 '17 at 14:30
  • Ok, but it seems like you would always need a conditional check somewhere to avoid the comma. How would you slice things to avoid that? – wally Sep 18 '17 at 14:36
  • @rex `print((x.specific_detail + ", " for x in vec[:-1]), end=""); print(vec[-1].specific_detail)` – Nick Chapman Sep 18 '17 at 14:40
  • 2
    @NickChapman [Boost::join](http://www.boost.org/doc/libs/1_41_0/doc/html/string_algo/reference.html#header.boost.algorithm.string.join_hpp) does this, including providing a user-defined predicate. – PaulMcKenzie Sep 18 '17 at 14:58
  • @NickChapman: You're correct. I remembered it wrong. – Fred Larson Sep 18 '17 at 14:58
  • Some good answers below, but the question already seems to have the closest equivalent to the Python code. This is turning into a useful question along the lines of: "is there a better way to avoid the trailing comma?". – wally Sep 18 '17 at 15:24
  • 2
    I did not see mention of: http://en.cppreference.com/w/cpp/experimental/ostream_joiner – Jeff Garrett Sep 18 '17 at 15:36
  • 1
    @JeffGarrett nobody who writes software in a production environment would ever use anything in experimental or -std=c++1z. It's asking for breakage down the line. Boost certainly, but incomplete standards, never. – Richard Hodges Sep 18 '17 at 16:52
  • @RichardHodges I claim that there exist production environments where it would be completely reasonable to rely on code that has gone through much ISO design process and been published by the committee, even though its interface may evolve later, likely in a different namespace. But I think it's worth mentioning as an option mostly as it is a standards-track solution for this problem. – Jeff Garrett Sep 18 '17 at 17:09
  • @RichardHodges, thank you. It drives me nuts when people are like why aren't you using this thing from C++2050 that was just discussed on Tuesday? – Nick Chapman Sep 18 '17 at 17:09

7 Answers7

23

One way of solving this I have seen is:

std::string separator;
for (auto x : vec) {
  ss << separator << x.specific_detail;
  separator = ",";
}
Chris Drew
  • 14,926
  • 3
  • 34
  • 54
  • 2
    This wastes time reinitializing the separator variable. – Interlooper Apr 21 '20 at 13:48
  • @Interlooper even if it's not optimised away by the compiler I suspect this is a very fast operation that doesn't break branch prediction etc. I suspect performance will be dominated by the streaming. But yes, you are right in theory, only way to know for sure is to measure. – Chris Drew Apr 21 '20 at 16:14
  • Agreed. However, would you think that an if statement checking if the iterator is at the end of the vector might be better than reinitializing a variable? – Interlooper Apr 21 '20 at 18:39
  • 1
    An `if` is exactly the sort of thing that will break branch prediction and impact performance significantly, plus you still have the operation to evaluate the `if` every loop. – Chris Drew Apr 21 '20 at 19:56
3

A fairly easy and reusable way:

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

And another way which defines an extendable protocol for printing containers:

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

...of course we can go further...

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

}

expected output:

[ 1,2,3,4,5 ]
( 1 : 2 : 3 : 4 : 5 )
>>>1-2-3-4-5<<<
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
2

Here is a tiny simple range library:

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

now we are ready.

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

Live example.

Now you can get fancier. boost transform iterators, together with boost range, let you do something similar to a list comprehension in python. Or Rangesv3 library for C++2a.

Writing a transform input iterator isn't amazingly hard, it is just a bunch of boilerplate. Simply look at the axioms of input iterator, write a type that stores an arbitrary iterator and forwards most methods to it.

It also stores some function. On * and ->, call the function on the dereferenced iterator.

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

Live example of transformer.

And if we add -- we can even use ostream iterator.

Note that std::prev requires a bidirectional iterator, which requires forward iterator concept, which requires that the transform iterator return an actual reference, which is a pain.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
2

Here's an example using 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';
}

Output:

Testing, One, Two, Three
7, 3, 3, 5
Fred Larson
  • 60,987
  • 18
  • 112
  • 174
1

You can use the exact code you already have, just change the type you pass to std::ostream_iterator to restrict its output:

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());

Live demo

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
0

Here's what was ultimately used

// 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;
Nick Chapman
  • 4,402
  • 1
  • 27
  • 41
-3

You can simply the exact same code, but define a operator<< overload:

ostream &operator<<(ostream& out)
{
    out << m_detail;
}