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?
There are two primary ways the standard uses to manipulate input & output operations.
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;
}
operator<<
for A
checks which format type is currently stored in the stream (0 for text, 1 for binary) and calls the corresponding methodtext
& binary
are the io manipulators that change the stream state when applied to a stream.Example Usage:
A a;
std::cout << text << a;
std::cout << binary << a;
std::cout << a; // still in binary format
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;
}
binary_impl
) that implements a different operator<<
for A
objects.Example Usage:
A a;
std::cout << text(a);
std::cout << binary(a);
std::cout << a; // default is text format
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();