Using std::streams to format output
I have an object that I want to be able to stream. But I want to be able to stream it in different ways by using different formats, or should I say ways to describe this object. And I wonder how this is supposed to be solved with streams.
What I want is to be able to use a generic format and use some kind of format adapter to transform the generic format into the preferred format.
I also want to be able to separate the format from the implementation of Item, so I do not have to change Item each time a new format is added or changed.
this code illustrate开发者_如何学Python approximately what I want.
Item item;
std::cout << "generic formatted:" << item;
std::cout << "custom formatted:" << CustomItemFormat() << item;
but this might not be possible or practical.
how is the streaming library intended to be used facing such problems?
Personally I would write a set of formatters.
The formatters have to know the internals of the object they are formatting
but making them friends should not be a big deal.
class X
{ friend std::ostream& operator<<(std::ostream& str,XML_Format const& formatter);
friend std::ostream& operator<<(std::ostream& str,Json_Format const& formatter);
friend std::ostream& operator<<(std::ostream& str,Fred_Format const& formatter);
public: int value() const {return 5;}
};
struct XML__Foram { X const& print; XML_Format(X const& v): print(v) {} };
struct Json_Format { X const& print; Json_Format(X const& v): print(v) {} };
struct Fred_Format { X const& print; Fred_Format(X const& v): print(v) {} };
std::ostream& operator<<(std::ostream& str,XML_Format const& formatter)
{
return str << "<XObj>" << formatter.print.value() << "</XObj>";
}
std::ostream& operator<<(std::ostream& str,Json_Format const& formatter)
{
return str << "{XObj:{" << formatter.print.value() << "}}";
}
std::ostream& operator<<(std::ostream& str,Fred_Format const& formatter)
{
return str << "Killl Kill Kill. Friday 13th";
}
int main()
{
X obj;
std::cout << XML_Format(obj) << std::endl;
}
Yes it's possible, the magic word you're looking for is stream manipulators.
Check out this question: C++ custom stream manipulator that changes next item on stream
You need to write a stream manipulator which stores information in the stream that is then used by operator<<(std::ostream&,const Item&)
. See the beginning of this answer for how to store user data in a stream.
IOStreams are not very well-suited for this, but you can use a formatting library such as {fmt} that provides better extensibility. For example:
struct Item {
int value;
};
template <>
struct fmt::formatter<Item> {
enum format { generic, custom };
format f = generic;
constexpr auto parse(parse_context &ctx) {
auto it = ctx.begin(), end = ctx.end();
if (it == end) return it;
if (*it == 'g') f = generic;
else if (*it == 'c') f = custom;
else return it;
return ++it;
}
template <typename FormatContext>
auto format(const Item& item, FormatContext &ctx) {
return format_to(ctx.out(), f == generic ? "{}" : "{:x}", item.value);
}
};
Then you can use Item
objects with any formatting function such as print
:
fmt::print("{}", Item{42}); // Default generic format - value formatted as decimal
fmt::print("{:g}", Item{42}); // Explicit generic format
fmt::print("{:c}", Item{42}); // Custom format - value formatted as hex
Disclaimer: I'm the author of {fmt}.
What is so bad about Item.CustomItemFormat()?
Alternatively there are some design patterns aimed at solving this problem, namely the visitor pattern.
You could also have CustomItemFormat(Item)?
I dont think that asking streams to solve this problem is the right orientation, you should delegate the task of showing itself to the Item class
The more I think about it I am starting to wonder if I am approaching this the wrong way. Perhaps I should have a generic format interface and setting the format for the item, so the item can output itself using this format.
Does this make sense?
Try using the Visitor design pattern:
struct Object_Writer_Interface
{
virtual void write_member_i(int value) = 0;
virtual void write_member_c(char value) = 0;
};
struct Object
{
int i;
char c;
void write(Object_Writer_Interface * p_writer)
{
if (p_writer)
{
p_writer->write_member_i(i);
p_writer->write_member_c(c);
}
}
};
struct Stream_Object_Writer
: public Object_Writer_Interface
{
Stream_Object_Writer(std::ostream& out)
: m_out(out)
{ ; }
void write_member_i(int value)
{
m_out << i << '\n';
}
void write_member_c(char c)
{
m_out << "'" << c << "'\n";
}
std::ostream& m_out;
};
int main(void)
{
Object o;
o.i = 5;
o.c = 'M';
// Create a writer for std::cout
Stream_Object_Writer writer(std::cout);
// Write the object to std::cout using the writer
o.write(&writer);
return EXIT_SUCCESS;
}
With this design, to write the objects to a socket, you derive a class from Object_Writer_Interface
to handle the socket. And similarly if you want to serialize the Object.
I'm currently using this technique for writing objects to GUI list boxes, database records and Xml files. Unlike the technique for overloading operator <<
, I didn't have to modify the principle class when developing new Writers.
精彩评论