Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ stream operator question

Tags:

c++

stream

csv

I suppose this might be simple question for all the gurus here but I somehow couldn't figure out the answer.

I want to be able to write csv cells to stream as simple as this:

stream << 1 << 2 << "Tom" << std::endl;

which would create output like 1,2,Tom. How can I achieve that? I figured that I need to create custom streambuf (as I don't think it's the right way to do it on stream level, it would be real pain just to overload << for all the types) but I'm not sure how << is normally implemented. Does it call put or write or what. Should I override those or what? Or did I just miss something completely?

I'd appreciate any help :)

Cheers,

like image 852
Tom Avatar asked Mar 04 '10 02:03

Tom


2 Answers

Getting something like 98% of the way there isn't terribly difficult:

#include <iostream>

class add_comma { 
    std::ostream &os;
    bool begin;
    typedef add_comma &ref;
public:
    add_comma(std::ostream &o) : os(o), begin(true) {}

    template <class T>
    ref operator<<(T const &t) { 
        if (!begin)
            os << ",";
        os << "\"" << t << "\"";
        begin = false;
        return *this;
    }

    ref operator<<(std::ostream &manip(std::ostream &o) ) {
        if (&manip == &std::endl)
            reset();
        manip(os);
        return *this;
    }

    void reset() { begin = true; }

    operator void *() { return (void *)os; }
};

int main() { 
    add_comma a(std::cout);

    a << 1 << 2 << "This is a string" << std::endl;
    a << 3 << 4 << "Another string" << std::endl;
    return 0;
}

Edit: I've fixed the code to at least some degree -- it now only puts commas between items that are written, not at the beginning of a line. It only, however, recognizes "endl" as signaling the beginning of a new record -- a newline in a string literal, for example, won't work.

like image 153
Jerry Coffin Avatar answered Nov 15 '22 18:11

Jerry Coffin


While I can appreciate the idea of overloading the stream operator, I would question the practice for the problem at hand.

1. Object-Oriented approach

If you are willing to write in a .csv file, then each line should probably have the very same format than the others ? Unfortunately your stream operator does not check it.

I think that you need to create a Line object, than will be streamable, and will validate each field before writing them to the file (and write them with the proper format). While not as fashionable, you'll have much more chance of achieving a robust implementation here.

Let's say that (for example) you want to output 2 integers and a string:

class Line
{
public:
  Line(int foo, int bar, std::string firstName):
    mFoo(foo), mBar(bar), mFirstName(firstName)

  friend std::ostream& operator<<(std::ostream& out, const Line& line)
  {
    return out << line.mFoo << ',' << line.mBar << ','
               << line.mFirstName << std::endl;
  }
private:
  int mFoo;
  int mBar;
  std::string mFirstName;
};

And using it remains very simple:

std::cout << Line(1,3,"Tom") << Line(2,4,"John") << Line(3,5,"Edward");

2. Wanna have fun ?

Now, this may seem dull, and you could wish to play and yet still have some control over what is written... well, let me introduce template meta programming into the fray ;)

Here is the intended usage:

// Yeah, I could wrap this mpl_::vector bit... but it takes some work!
typedef CsvWriter< mpl_::vector<int,int,std::string> > csv_type;

csv_type(std::cout) << 1 << 3 << "Tom" << 2 << 4 << "John" << 3 << 5 << "Edward";

csv_type(std::cout) << 1 << 2 << 3; // Compile Time Error:
                                    // 3 is not convertible to std::string

Now that would be interesting right ? It would format the line and ensure a measure of validation... One could always complicate the design so that it does more (like registering validators for each field, or for the whole line, etc...) but it's already complicated enough.

// namespace mpl_ = boost::mpl

/// Sequence: MPL sequence
/// pos: mpl_::size_t<N>, position in the Sequence

namespace result_of {
  template <class Sequence, class pos> struct operator_in;
}

template < class Sequence, class pos = mpl_::size_t<0> >
class CsvWriter
{
public:
  typedef typename mpl_::at<Sequence,pos>::type current_type;
  typedef typename boost::call_traits<current_type>::param_type param_type;

  CsvWriter(std::ostream& out): mOut(out) {}

  typename result_of::operator_in<Sequence,pos>::type
  operator<<(param_type item)
  {
    typedef typename result_of::operator_in<Sequence,pos>::type result_type;

    if (pos::value != 0) mOut << ',';
    mOut << item;

    if (result_type::is_last_type::value) mOut << std::endl;              

    return result_type(mOut);
  }

private:
  std::ostream& mOut;
}; // class CsvWriter


/// Lil' bit of black magic
namespace result_of { // thanks Boost for the tip ;)

  template <class Sequence, class pos>
  struct operator_in
  {
    typedef typename boost::same_type<
        typename mpl_::size<Sequence>::type,
        typename mpl_::next<pos>::type
      > is_last_type;

    typedef typename mpl_::if_<
      is_last_type,
      CsvWriter< Sequence, mpl_::size_t<0> >,
      CsvWriter< Sequence, typename mpl_::next<pos>::type >
    >::type;
  }; // struct operator_in<Sequence,pos>

} // namespace result_of

Here you have a stream writer that ensures that the cvs file is properly formatted... baring newlines characters in the strings ;)

like image 43
Matthieu M. Avatar answered Nov 15 '22 17:11

Matthieu M.