c++streammanipulators

How to create a manipulator that would call a specific function in the next object in the stream?


Suppose I have a class as follows:

class A
{
...private members
public:
void write_text(std::ostream& os); //writes members as text
void write_binary(std::ostream& os); //writes objects as binary
};

How do I create a manipulator that like text and binary depending on which I can call appropriate function write_text() or write_binary() to write to filestream like so:

std::ofstream file1("textfile.txt");
std::ofstream file2("binfile.bin");

A obj; // assume obj has data members set 
file1<<text<<obj; // here obj.write_text() should be invoked 
file2<<binary<<obj; // here obj.write_binary() should be invoked

Do I need to store something like a state or a variable in the stream like in this example to be able to do this or is there a simpler way?


Solution

  • There are two primary ways the standard uses to manipulate input & output operations.

    1. Storing values in the stream state

    You can store formatting state within streams by using std::ios_base::xalloc().
    This gives you a long and void* value in each stream that you can access with iword() / pword() .

    This is the same mechanism that standard io manipulators like std::hex, std::boolalpha use.

    Note that if you change the stream state it'll stay that way until you change it again, e.g.:

    std::cout << std::hex << 16; // will be outputted in hexadecimal
    std::cout << 12; // will still be outputted in hexadecimal
    
    std::cout << std::dec << 16; // will be outputted in decimal
    std::cout << 12; // still decimal
    

    You could e.g. implement it like this for your A class:

    
    class A {
    public:
        void write_text(std::ostream& os) const {
            os << "TEXT";
        }
    
        void write_binary(std::ostream& os) const {
            os << "BINARY";
        }
    };
    
    // this gives us the unique index we need for pword() / iword()
    inline int getAFormatIndex() {
        static int idx = std::ios_base::xalloc();
        return idx;
    }
    
    std::ostream& operator<<(std::ostream& os, A const& a) {
        std::ostream::sentry s{os};
        if(!s) return os;
        
        if(os.iword(getAFormatIndex()) == 0)
            a.write_text(os);
        else
            a.write_binary(os);
    
        return os;
    }
    
    struct text_t {};
    struct binary_t {};
    
    inline constexpr text_t text;
    inline constexpr binary_t binary;
    
    // change to text mode
    std::ostream& operator<<(std::ostream& os, text_t const&) {
        os.iword(getAFormatIndex()) = 0;
        return os;
    }
    
    // change to binary mode
    std::ostream& operator<<(std::ostream& os, binary_t const&) {
        os.iword(getAFormatIndex()) = 1;
        return os;
    }
    

    Example Usage:

    A a;
    std::cout << text << a;
    std::cout << binary << a;
    std::cout << a; // still in binary format
    

    godbolt example

    2. Wrapper function

    Another kind of io manipulators you'll also encounter in the standard library are wrappers that change the input / output of a single element.

    Examples of this would be std::quoted, std::get_money, std::put_money, etc...

    Those functions only change the format for a single operation, in contrast to the above method that changes the format of all following input / output operations. Example:

    std::cout << std::put_money(12.34); // will be formatted as monetary value
    std::cout << 12.34; // normal double output
    std::cout << std::quoted("foo"); // -> "foo"
    std::cout << "foo"; // -> foo
    

    You could e.g. implement it like this for your A class:

    
    class A {
    public:
        void write_text(std::ostream& os) const {
            os << "TEXT";
        }
    
        void write_binary(std::ostream& os) const {
            os << "BINARY";
        }
    };
    
    std::ostream& operator<<(std::ostream& os, A const& a) {
        std::ostream::sentry s{os};
        if(!s) return os;
    
        a.write_text(os);
    
        return os;
    }
    
    struct binary_impl { A const& a; };
    
    std::ostream& operator<<(std::ostream& os, binary_impl const& b) {
        std::ostream::sentry s{os};
        if(!s) return os;
    
        b.a.write_binary(os);
    
        return os;
    }
    
    binary_impl binary(A const& a) {
        return { a };
    }
    
    // text is the default, so we need no wrapper
    A const& text(A const& a) {
        return a;
    }
    

    Example Usage:

    A a;
    std::cout << text(a);
    std::cout << binary(a);
    std::cout << a; // default is text format
    

    godbolt example


    The methods listed above are only the ones the standard library itself uses (and therefore probably the most recognized ones).

    You can of course also create your own custom method for it, e.g. by using member methods that return objects that will serialize the object in a specific way:

    class A {
    public:
        void write_text(std::ostream& os) const {
            os << "TEXT";
        }
    
        void write_binary(std::ostream& os) const {
            os << "BINARY";
        }
    
        struct as_text_t { A const& a; };
        struct as_binary_t { A const& a; };
    
        as_text_t as_text() const {
            return { *this };
        }
    
        as_binary_t as_binary() const {
            return { *this };
        }
    };
    
    std::ostream& operator<<(std::ostream& os, A::as_text_t const& el) {
        std::ostream::sentry s{os};
        if(!s) return os;
    
        el.a.write_text(os);
    
        return os;
    }
    
    std::ostream& operator<<(std::ostream& os, A::as_binary_t const& el) {
        std::ostream::sentry s{os};
        if(!s) return os;
    
        el.a.write_binary(os);
    
        return os;
    }
    

    Usage:

    A a;
    std::cout << a.as_text();
    std::cout << a.as_binary();
    

    godbolt example