6
\$\begingroup\$

I sometimes find myself changing stream settings (e.g. to output hexadecimal, or to change fill character), and needing to change those settings back afterwards, to avoid perturbing subsequent output. This gets tedious, so I wrote a short class to act as a scope guard, restoring the settings when it is destructed.

Now we can "borrow" an input or output stream, and give it back in the condition we found it.

#include <ios>
#include <iomanip>

// Members are all public and mutable, so if we really don't want
// to restore any particular part of the state, we can override.
template<class CharT, class Traits = typename std::char_traits<CharT>>
struct save_stream_state
{
    std::basic_ios<CharT,Traits>& stream;
    std::ios_base::fmtflags flags;
    std::locale locale;
    std::streamsize precision;
    std::streamsize width;
    CharT fill;

    save_stream_state(std::basic_ios<CharT,Traits>& stream)
        : stream{stream},
          flags{stream.flags()},
          locale{stream.getloc()},
          precision{stream.precision()},
          width{stream.width()},
          fill{stream.fill()}
    {}

    // deleting copy construction also prevents move
    save_stream_state(const save_stream_state&) = delete;
    void operator=(const save_stream_state&) = delete;

    ~save_stream_state()
    {
        stream.flags(flags);
        stream.imbue(locale);
        stream.precision(precision);
        stream.width(width);
        stream.fill(fill);
    }
};

It's not strictly necessary to delete operator=, as the compiler won't generate one for a class with a non-static reference member, but I think it's better to be explicit there.


Here's a simple test program that shows how it's used. If you are using an old standards version, the template type deduction won't work - you'll need to specify the guard type in full (save_stream_state<char> and save_stream_state<wchar_t>).

#include <iostream>
int main()
{
    auto test = []() {
        std::cout << std::setw(15) << "Foo" << ' '
        << true << ' ' << 123456 << '\n';
    };
    {
        test();
        const save_stream_state guard{std::cout};
        std::cout << std::setfill('_') << std::left << std::uppercase
                  << std::boolalpha << std::hex << std::showbase;
        test();
    } // stream restored here
    test();


    std::cout << std::endl;


    // Now with wide-character stream:
    auto wtest = []() {
        std::wclog << std::setw(15) << L"Foo" << L' '
        << true << L' ' << 123456 << L'\n';
    };
    {
        wtest();
        // AAA style initialization - and guard multiple streams
        auto const guard = { save_stream_state{std::wclog},
                             save_stream_state{std::wcin} };
        std::wclog << std::setfill(L'_') << std::left << std::uppercase
                   << std::boolalpha << std::hex << std::showbase;
        wtest();
    } // stream restored here
    wtest();
}
\$\endgroup\$
  • 2
    \$\begingroup\$ I have come to the conclusion that the statefulness of standard streams to too insidious of a blunder to work around. What's needed is to separate the concerns of formatting and streaming. At first I used boost::format. Now I am using fmtlib/fmt. It is a brilliant product, open source, and free. github.com/fmtlib/fmt \$\endgroup\$ – Jive Dadson Mar 16 '18 at 22:55
  • \$\begingroup\$ I do see a use though. If there is legacy code that leaves a stream in an unwanted state, this could be used to save the the state before calling it and restore when it returns. \$\endgroup\$ – Jive Dadson Mar 16 '18 at 23:03
  • 1
    \$\begingroup\$ fmt looks like something I'll be using in future, particularly as it's likely to be part of C++20. \$\endgroup\$ – Toby Speight Mar 19 '18 at 11:41
3
\$\begingroup\$

I agree with everything what @JiveDadson said in the comment. I believe there are some ways to improve current code.

## Syntax

To use the guard, people will need to introduce a scope. My desired syntax would be:

stream << session << settings << data;

I'll present solution with the syntax above later.

My solution seem to solve only a subset of problems which original code does. Thus my point about syntax is not valid. Example can be multiple statement prints, in which case my solution would do unnecessary work.

Naming

Would it be better to use stream_state_guard? The code uses name guard in the tests. It is funny that tests reflect what was actually intended to be written. I found that it is the case in my code too.


Alternative solution

To achieve the syntax above, I've used some of the new features of C++17. My thoughts are a mess more than usual, so watch out for potential nitpicks.

First of all, session needs to be object of some empty type, just like nullptr is to nullptr_t or nullopt is to nullopt_t.

struct session_t {};
static inline constexpr session_t session;

inline is used to deal with linker.

Now, the class itself.

#include <ios>
#include <iomanip>
#include <ostream>

class session_stream
{
    std::ostream& stream;
    std::ios_base::fmtflags flags;
    std::locale locale;
    std::streamsize precision;
    std::streamsize width;
    char fill;

    session_stream(std::ostream& stream)
        : stream{stream},
          flags{stream.flags()},
          locale{stream.getloc()},
          precision{stream.precision()},
          width{stream.width()},
          fill{stream.fill()}
    {}

public:
    friend session_stream operator<<(std::ostream& os, session_t);

    // deleting copy construction also prevents move
    session_stream(const session_stream&) = delete;
    void operator=(const session_stream&) = delete;

    template <typename T>
    std::ostream& operator<<(T&& value)
    {
        stream << std::forward<T>(value);
        return stream;
    }


    ~session_stream()
    {
        stream.flags(flags);
        stream.imbue(locale);
        stream.precision(precision);
        stream.width(width);
        stream.fill(fill);
    }
};

The code is just adjusted version from the question. Interesting things to note:

  • Constructor and copy semantics are private

    Wrong usage is impossible through conventional means. People will need to be either too smart or cause undefined behavior.

  • Create an object only by using stream << session

    This might look fishy. Especially this line:

    return session_stream{os};
    

    Since C++17 this is guaranteed copy elision, thus it works even if copy constructor is unavailable. I don't think that guarantees impossibility of prolonging lifetime of session_stream, but it will guard against unintentional wrong usages.

  • Uses its own operator<< only on the first call

    I wasn't sure if overload resolution and ADL won't intervene, so I decided to return ostream& on operator<<.

Rewritten tests:

#include <iostream>

int main()
{
    auto test = []() {
        std::cout << std::setw(15) << "Foo" << ' '
                  << true << ' ' << 123456 << '\n';
    };
    {
        test();
        std::cout << session << std::setfill('_') << std::left << std::uppercase
                  << std::boolalpha << std::hex << std::showbase << std::setw(15) 
                  << "Foo" << ' ' << true << ' ' << 123456 << '\n';
        test();
    } // stream restored here
    test();
    std::cout << std::endl;

}

Live on Wandbox.

And just for reference: sessions can be nested.

\$\endgroup\$
  • 1
    \$\begingroup\$ About fmtlib: here is video about it on cppcon2017. Author seems to blog about standardization process. I haven't used it in professional environment yet, as I don't have a job at all, but I've heard consistent positive things about it on stackoverflow. Please consider voting for it during SG meeting. \$\endgroup\$ – Incomputable Mar 18 '18 at 16:57
  • \$\begingroup\$ Also, if you're using my solution, please put it in a namespace. I had ADL to nuke me with 17 pages of error messages. \$\endgroup\$ – Incomputable Mar 18 '18 at 16:59
  • \$\begingroup\$ Interesting idea, and I learned a lot from this. It seems possibly harder to use because you have to have all the output in a single expression; you can't { os << session; for (auto const& element: container) os << element; } (but if the for loop is in a function you could pass os << element as its argument; or you could just introduce a temporary). Oh, and I think it might be better if it were templated - your version works only on output streams, but mine also handles input streams. \$\endgroup\$ – Toby Speight Mar 19 '18 at 8:50
  • \$\begingroup\$ @TobySpeight, I just wanted to show fancy syntax. I don't really want to deal with iostreams anymore. I hope that fmt will make it in 2020. If you've noticed, I tried to replicate the fmt's syntax for years. It's just that somebody have done it long ago. \$\endgroup\$ – Incomputable Mar 19 '18 at 12:01
  • \$\begingroup\$ It appears that fmt really is coming with C++20, so hold your fingers crossed for just another year or two! :-) \$\endgroup\$ – Toby Speight Nov 22 '18 at 14:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.