Search code examples
c++operator-overloadingstringstream

How to overload operator<<(double const&) in an inherited std::stringstream?


I would like to overwrite the operator<< such that:

double d = 3.0;
mycustomstringstream << "Hello World " << d << "what a nice day.";
std::cout << mystream.str() << std::endl;

will produce the output:

Hello World (double)(3.00000000000)what a nice day.

Remark to avoid confusion: Every double shall be printed this way then, with a "(double)(" in front and a ")" behind, whenever a stream implementing my operator is used. The stream should come at least with all the functionality of string-stream, and I prefer to not have to fully re-implement the streamby myself. The purpose of the question is not for exercise but to save time/tediousness/errors from otherwise having to wrap a function call double_to_string around every streamed double d.

What I tried

Overriding

Since I would be fine for doubles to be printed like so, I boldly implemented:

std::ostream& operator<<(std::ostream& o, double const& d){
  o<<"(double)("<< d << ")";
  return o;
}

That does not work because the compiler explains about ambiguity (since that operator is already defined).

Inheriting

So as in the beginning, I am fine to inherit from std::stringstream and just replace the definition of that operator for my customstringstream:

#include <sstream>

class MyCustomStringStream: public std::stringstream{};

MyCustomStringStream& operator<<(MyCustomStringStream& o, double const& d){
  o<<"(double)("<< ( (std::stringstream)(o) << d ) << ")";
  return o;
}

Here is the error:

error: use of deleted function 'std::__cxx11::basic_stringstream<_CharT, _Traits, _Alloc>::basic_stringstream(const std::__cxx11::basic_stringstream<_CharT, _Traits, _Alloc>&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]'

So then instead:

#include <iostream>
#include <sstream>

class MyStringStream: public std::stringstream{
    std::stringstream aux;
public:
    MyStringStream& operator<<(double const& d){
        aux.str() = "";
        aux << std::scientific << "((double)(" << d << ")";
        *this << aux.str();
        return *this;
    }
};

int main() {
    double d = 12.3;
    MyStringStream s;
    s << "Hello World " << d << "what a nice day.";
    std::cout << s.str() << std::endl;
}

But this still prints:

Hello World 12.3what a nice day.

Demo


Solution

  • Streams are designed to support this sort of customization, though they way they support it may not initially be as obvious as you'd like (and implementing it correctly is kind of a pain too).

    When you write a number to a stream, the formatting is delegated to the stream's locale. That locale contains (among other things) a num_put facet, which is what actually formats a number as desired for the stream.

    The num_put facet has overloads of do_put for each of the standard numeric types, including double. Since you (apparently) want to leave other types formatted normally, and apply your special formatting only to doubles, you override only the do_put overload for double.

    To format a number with this, you imbue your stream with a locale that contains an instance of that num_put facet:

      std::ostringstream os;
    
      std::locale loc(std::locale::classic(), new my_num_put<char>);
    
      os.imbue(loc);
    

    Then write a double to the stream:

      os << 123.456 << "\n";
    
      // and copy the result to standard output so we can see it:
      std::cout << os.str();
    

    For a proof of concept, your do_put can use a stringstream to convert the double to a string, then copy the prefix, string, and suffix to the output sequence. Moving beyond proof of concept is a bit more work though.

    To format the number correctly, we need to get all the formatting information from the ios_base that's passed to do_put, and set the formatting for our temporary stringstream accordingly. For example:

        std::ostringstream temp;
    
        // copy flags, width and precision:
        temp.flags(b.flags());
        temp.width(b.width());
        temp.precision(b.precision());
        
        temp << v;
    

    Figuring out everything that needs to be copied over to do all formatting correctly is left as an exercise for the interested reader. Here's a quick test program with semi-complete code as described above:

    #include <locale>
    #include <sstream>
    #include <ios>
    #include <limits>
    #include <iostream>
    #include <iomanip>
    
    template <class charT, class OutputIterator = std::ostreambuf_iterator<charT>>
    class my_num_put : public std::num_put<charT, OutputIterator> {
    public:
      using iter_type = typename std::num_put<charT, OutputIterator>::iter_type;
      using char_type = typename std::num_put<charT, OutputIterator>::char_type;
    
      iter_type do_put(iter_type i, std::ios_base &b, char_type fill, double v) const override {
        std::string prefix = "(double)(";
        std::string suffix = ")";
    
        std::ostringstream temp;
        temp.flags(b.flags());
        temp.width(b.width());
        temp.precision(b.precision());
        temp.fill(fill);
        
        temp << v;
        std::string const &s = temp.str();
    
        std::copy(prefix.begin(), prefix.end(), i);
        std::copy(s.begin(), s.end(), i);
        std::copy(suffix.begin(), suffix.end(), i);
        return i;
      }
    };
    
    int main() {
    
      std::ostringstream os;
    
      std::locale loc(std::locale::classic(), new my_num_put<char>);
    
      os.imbue(loc);
    
      // test a minimal case:
      os << 1.234 << "\n";
     
      // test with a few formatting flags:
      os << std::setw(17) << 123.456 << "\n";
      os << std::setw(14) << std::setprecision(12) << std::left << 9876.54321 << "\n";
      std::cout << os.str();
    }
    

    The result I get is about as you'd apparently hope/expect:

    (double)(1.234)
    (double)(          123.456)                
    (double)(9876.54321    )
    

    This should work with essentially any stream, not just a stringstream. And once you've imbued a stream with this locale, you don't have to make any further changes to other code for all doubles you write to that stream to be formatted this way.

    For the moment, I've left the precision and width applying only to the width and precision used for the number itself, so when you write something out with std::setw(10), the output will be 10 + the size of prefix and suffix you use. Depending on your viewpoint, it may well make more sense to subtract the size of your prefix+suffix from the width applied to the number itself, so the width will be the total field size, not just the size of the number part.

    So, while I'd still consider it a proof of concept rather than finished, fully-tested code, I'd say this does give a basic idea of an approach that should be able to accomplish your goal (and depending on how lofty a goal you've setting, may already be adequate).