☑ C++ Streaming Pains

20 Aug 2015 at 1:55PM in Software
 | 
Photo by Tim Gouw on Unsplash
 | 

C++ streams have their advantages, but in some ways they’re a downright pain.

frustrated coder

Some time ago I wrote a post called The Dark Arts of C++ Streams. The name was a reference to the fact that, as a C programmer who later learned C++, I’d always regarded them with some suspicion and disdain as a much more cumbersome alternative to the likes of printf(). Of course, streams are much more suited to C++ where behaviours may be extended and overridden as needed to support user-specified types, and the output system needs to support that — but for the common cases of printing formatted output of strings, integers and floats, printf() just seemed much more convenient.

Since writing that post I’ve become rather more familiar with streams, and even grown fond of some of their quirks — their flexibility is useful for things like debugging and their comparative verbosity has lost much of its sting. There are still some things that bug me about streams, however — mostly a set of related traps that are easy to fall into and could, to my mind at least, have easily been avoided.

The main issue I have rests on the statefulness of the formatting. Let’s say you want to output a floating point value to 3 decimal places, and then another to 5 decimal places. Each time you need to set the precision on the stream and not on the value being streamed out. This is possible with explicit methods on the stream:

std::cout.setf(std::ios::fixed);
std::cout.precision(3);
std::cout << 123.456789 << std::endl;  // 123.456

However, this is rather awkward — one of the big syntactic advantages of streams is the way that they can be chained. Taking the previous example of two floats, you’d like to be able to do something like this:

std::cout << first << " and " << second << std::endl;

Indeed you can do that — but now add some formatting using the method-based approach earlier and it’s absolutely horrible:

std::cout.setf(std::ios::fixed);
std::cout.precision(3);
std::cout << first << " and ";
std::cout.precision(5);
std::cout << second << std::endl;

Of course, as anyone who’s done this sort of formatting will probably know, it is possible to do the whole thing inline:

std::cout << std::fixed
          << std::setprecision(3) << first
          << " and "
          << std::setprecision(5) << second
          << std::endl;

That’s conceptually a lot nicer, but the spanner in the works is the fact that these functions are just a fancy shortcut for calling the methods on the stream itself. In other words the formatting changes they make are not scoped in any way — they persist on the stream until explicitly replaced.

This could lead to all sorts of nasty consequences. Imagine code like this:

// Add a trace comment to the output file in a debug build.
void traceFloat(std::ostream& out, std::string name, double value)
{
#ifdef DEBUG
    out << "# The value of '" << value << "' is "
        << std::fixed << std::setprecision(16) << value
        << std::endl;
#endif /* ifdef DEBUG */
}

// ... Later on in the source file...

void myFunction()
{
    double foo_value = getFooValue();
    // ...
    myFile.precision(3);
    traceFloat(myFile, "foo", foo_value);
    myFile << "foo:" << foo_value << "\n";
    // ...
}

Here we have some routine which is generating some sort of formatted file, but in a debug build (only) it also adds some comments for diagnostics. Except in this particular case, the act of adding the diagnostic actually changes the output format of the file — the use of setprecision() in the traceFloat() function overrides the earlier setting of precision() within myFunction(). This is something that’s just begging to cause those annoying problems that only materialise in a production environment.

This is a fairly contrived example, but this sort of action at a distance is really poor practice. Of course all of this is documented, but an important property of a programming language (and I always include standard libraries in that term) is how easily programmers can avoid or detect both current and potential future mistakes.

All this said, that’s the way it works and it’s unlikely to change in any fundamental manner — so is there a way to use streams safely? Well, as is so often the case in C++ RAII comes to the rescue.

It’s possible to capture the current state of the stream and restore it later, which is probably best practice for anyone using streams for any purpose where the format is critical.

It’s not hard to build a class which uses an RAII approach to save and later restore the current state of the stream:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class StreamStateGuard
{
  public:
    StreamStateGuard(std::ios_base& stream) : stream_(stream)
    {
        captureStreamState();
    }

    ~StreamStateGuard()
    {
        restoreStreamState();
    }

    void captureStreamState()
    {
        precision_ = stream_.precision();
        width_ = stream_.width();
        flags_ = stream_.flags();
    }

    void restoreStreamState()
    {
        stream_.precision(precision_);
        stream_.width(width_);
        stream_.flags(flags_);
    }

  private:
    std::ios_base& stream_;
    std::streamsize precision_;
    std::streamsize width_;
    std::ios_base::fmtflags flags_;
};

This is for illustrative purposes and I haven’t tested it extensively, so there may be additional state to save I’ve forgotten or exceptions I haven’t handled, but it’s pretty simple. If you’re lucky enough to be able to use Boost in your C++ environment then it already has classes to do all this for you.

Let’s say you’re writing some code to output formatted values in some defined text file format — you could write a function to output a row which saves the state at the start of the function and restores it at the end. However, wouldn’t it be nice if each field could do that itself? That would keep things modular, and it’s something we can do by writing new IO manipulators. I should mention here that this is something I’ve done little of, so there may be more elegant approaches, but the code below seems to work:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class MyClass
{
  public:
    MyClass(double value1, double value2)
            : value1_(value1), value2_(value2)
    { }

    void writeValues(std::ostream& out);

  private:
    class outputField
    {
      public:
        outputField(double value, int decimal_places, int width)
                : value_(value),
                  decimal_places_(decimal_places),
                  width_(width)
        { }
        std::ostream& operator()(std::ostream& out) const;

      private:
        double value_;
        int decimal_places_;
        int width_;
    };

    friend std::ostream& operator<<(
            std::ostream& out,
            const MyClass::outputField& item);

    double value1_;
    double value2_;
};

std::ostream& operator<<(std::ostream& out, const MyClass::outputField& item)
{
    return item(out);
}

std::ostream& MyClass::outputField::operator()(std::ostream& out) const
{
    StreamStateGuard streamStateGuard(out);

    return out << std::fixed << std::noshowpoint
               << std::setprecision(decimal_places_)
               << std::setw(width_)
               << value_;
}

void MyClass::writeValues(std::ostream& output)
{
    output << "MyClass|"
           << outputField(value1_, 2, 8) << "|"
           << outputField(value2_, 6, 12) << "|\n";
}

The sample MyClass has only two double values to output, but this example could easily be extended to more or less any types.

These sorts of techniques start to illustrate what I’m increasingly learning as the best practices to keep a programmer sane when using C++ IO streams. I still have a soft spot for the old stdio.h functions, but I suppose all of us have to be dragged into the future at some point — at least now I’ve got a few crude tools to help me survive there.

20 Aug 2015 at 1:55PM in Software
 | 
Photo by Tim Gouw on Unsplash
 |