What bad things would happen if `std::ostringstream` had an `operator <<` which returns `std::ostringstream &`?

104 views Asked by At

In some languages like Ruby, there is fancy syntax for creating formatted output as a "one-liner" in part because of how commonly this is needed in Ruby applications.

It's also commonly needed in some C++ applications. Here's some example code:

if (num_doggies > num_biscuits) {
  std::ostringstream ss;
  ss << "There are " << num_doggies" << " puppy dogs and only " << num_biscuits << " biscuits!";
  log_warn(ss.str());
  this->negotiate_biscuit_reduction();
}

Some open source C++98 projects have a workaround that looks like this:

struct string_formatter {
  std::ostringstream ss;

  template <typename T>
  string_formatter & operator << (const T & t) {
    ss << t;
    return *this;
  }

  operator std::string() const {
    return ss.str();
  }
};

So that they can write something like this:

if (num_doggies > num_biscuits) {
  log_warn(string_formatter{} << "There are " << num_doggies" << " puppy dogs and only " << num_biscuits << " biscuits!");
  this->negotiate_biscuit_reduction();
}

turning three lines into one line.

However, the string_formatter class looks fairly evil from a code-review standpoint, mainly because of this implicit conversion. Implicit conversions are usually considered pretty evil, and they are more evil when they are converting to common or primitive types like int or std::string.

In C++11 we can change it slightly:

  operator std::string() && {
    return ss.str();
  }

Now the conversion will only trigger when the string_formatter is an r-value reference. So it's less likely to trigger, but it's also more complicated. Is it actually less evil? Debatable.

Also worth mentioning: others have gone to even more elaborate hacks to get this to work without a string_formatter shim: https://codereview.stackexchange.com/questions/6094/a-version-of-operator-that-returns-ostringstream-instead-of-ostream

No implicit conversions here, but we are overloading operators on standard library types which is usually a no-no.


Let's back up a bit. Why can't I compromise and do this? It's at least a little more compact:

if (num_doggies > num_biscuits) {
  std::ostringstream ss;
  log_warn((ss << "There are " << num_doggies" << " puppy dogs and only " << num_biscuits << " biscuits!").str());
  this->negotiate_biscuit_reduction();
}

It turns out that this doesn't compile, because std::ostringstream::operator << returns std::ostream & and not std::ostringstream &. By the time all the << have resolved, we only have an ostream & so we can't use .str().

My question is, why is that the case?

Why doesn't std::ostringstream::operator << return std::ostringstream &? Is it:

  • Undesirable for technical or safety reasons
  • Adding unnecessary difficulty for standard library implementors
  • Legacy -- it was always this way and changing it now might break code. (However, exactly what code would be broken by this? Since std::ostringstream & can be converted to std::ostream & by a standard conversion because of inheritance relationship?)
  • Obscurity -- no one cares!

Note: I'm well aware of different ways to design string_formatter and logger classes. I'm specifically interested in whether there are design reasons in the standard library that make it much better for std::ostringstream to return std::ostream &.

1

There are 1 answers

0
R Sahu On

It's not clear from your post whether you would care what the operato<< functions return if you can simplify your code that deals with logging the warning messages.

Here's one way you can simplify the code that logs warning messages.

#include <iostream>
#include <sstream>
#include <string>

void log_warn(std::string const& s)
{
   std::cout << s;
}

struct log_warning_impl
{
  mutable std::ostringstream ss;

  ~log_warning_impl()
  {
     log_warn(ss.str());
  }
};

template <typename T>
log_warning_impl const& operator<<(log_warning_impl const& l, const T & t)
{
   l.ss << t;
   return l;
}

log_warning_impl const& operator<<(log_warning_impl const& l, std::ostream&(*f)(std::ostream&))
{
   l.ss << f;
   return l;
}


#define WARNING_LOGGER log_warning_impl{}

int main()
{
   WARNING_LOGGER << "There is a problem in the " << 10 << "-th line of the data file" << std::endl;
}

In your case, you can simply use:

if (num_doggies > num_biscuits)
{
   WARNING_LOGGER << "There are " << num_doggies << " puppy dogs and only " << num_biscuits << " biscuits!";
}