Format C++ objects as you write them

The Standard Template Library allows you to write objects to streams. But what about custom formatting?'s C++ guru, John Torjo, walks you through sample code to demonstrate a solution.

The C++ Standard Template Library (STL) provides an elegant way to write objects to streams: You have a class, and to be able to write objects of its type to streams, you just define operator <<. However, you’ll soon realize that this is too general for any production code. Usually, only at the point of writing will you know how you want to format your object(s) prior to printing them. If you’re not yet convinced, here’s an example.

Say you have a date/time class and a user-friendly application that manages stock operations. When generating stock operation reports, you’ll want to customize the printed format of the time each operation occurred:
  • ·        For reports regarding the previous week, you might write times like Sun, 23:03
  • ·        For yearly reports, you might write something like Sun/15 Sep 2002.
  • ·        For short-period reports, you could use something like Sunday/15 Sep 02 (23:03).

Some of your customers may have relied on legacy applications before, and their reports used a custom time format. To make your application even easier for them to use, you'll want to allow them to print the time using that custom format. Sounds interesting? Read on; you’ll be pleasantly surprised at how easy C++ makes this customization process. As a bonus, I’ll throw a time formatter that does all of the above and much more.

Formatting your objects
The formatting concept is similar to the concept behind the ubiquitous printf function. You have an object and a format string. Within the format string, you have escape sequences (formatter IDs) that identify data/ properties that can be printed for this object.

Let's see how you can easily format objects:
  • ·        You have a class and need to format an object of its type, so you’ll have to apply a formatter.
  • ·        The formatter needs to know what data/properties can be printed for this class.
  • ·        You use macros to "export" what data/properties your class can print. A unique string (the formatter ID) identifies each data/property you're exporting. If you have experience with the Active Template Library/Microsoft Foundation Classes and the Active Template Library (ATL/MFC), you're already used to the MESSAGE_MAP macros. Exporting the data/properties is similar to this.
  • ·        When wanting to write an object of your class, you call the formatter, passing the object to write, and a format string, which contains embedded formatter IDs. Each formatter ID (identifying the data/property to write) is surrounded by { and }.

An example should help you better understand the process:

Step 1: You have the DateTime class. Given a DateTime object, you can write the:
  • ·        Day of Month (1-31), Day of Year (1-365), Day of Week (Mon-Sun)
  • ·        Week
  • ·        Hour (0-23), Hour (1-12), Minute, Second, AM/PM suffix, etc.

Step 2: You export the above formatter IDs:
  • ·        “DD” (Day of Month), “day_idx” (Day of Year)
  • ·        “weekday” (Day of week)
  • ·        “hh” (Hour, 0-23), “hh12” (Hour, 1-12)
  • ·        “mm” (Minutes), “ss” (Seconds), “ampm” (AM/PM), etc.

Step 3: To write a Time value like Sun (23:03) you will use format_obj(value, “{weekday} ({hh}:{mm})”).

Using the object formatter
Say you have a class, and you want to be able to print information using a custom format, as with our stock operations report example earlier. Here’s what you’ll do.

Step 1: Derive your class from obj_formatter
Derive your_class from obj_formatter< your_class, char_type>.

The obj_formatter internally holds each formatter ID and specifies what it should do when it encounters it.

The char_type is the type of characters your class uses to print the formatted information. For example, if your class might need Unicode characters, you will use wchar_t as the char type. It defaults to char.

Step 2: Define what your_class can print
Inside your class definition, define a map of what your class can print:
  • ·        Start the map with BEGIN_FORMATTER_MAP (your_class_name).
  • ·        End the map with END_FORMATTER_MAP().
  • ·        For each value your class can print, use one of the macros shown below.

Each value your class can print can be either:
  • ·        A data member (example: m_details.tm_year).
  • ·        A function that takes no parameters and returns a value, such as full_month().

When printing a value, you may want an additional formatting instruction (default formatting prints each value as is). For example, when printing a number, you might want a fixed number of digits. In this case, just provide an additional value formatter.

To format a value, use the following macros:
  • ·        FORMAT_VALUE( value, formatterID, formatterDescription, idx) formats the given value having the given formatter ID and a description for the formatter. The index starts from 1; increment it for each value you want to print.
  • ·        FORMAT_FORMATTED_VALUE( value, formatterID, formatterIDDescription, valueFormatter, idx) is the same as above except that we use the valueFormatter to format the value prior to writing it.
  • ·        FORMAT_FROM_OTHERS( fromOthersString, formatterID, formatterDescription, idx) is the same as FORMAT_VALUE, but instead of writing the value, it writes a combination of other values.
  • ·        FORMAT_FORMATTED_FROM_OTHERS( fromOthersString, formatterID, formatterDescription, valueFormatter, idx) functions the same as the above macro, but you employ the valueFormatter to format the value prior to writing it.

Listing A contains an example of using the above macros, and Listing B shows an example of a value formatter, format_number, that the STL provides by default.

Step 3: Write objects of your_class
You will use format_obj function, passing an object of your class, and the format string. Listing C shows a few examples of its usage.

The class’s internals
Your class is derived from the obj_formatter class. Internally, obj_formatter implements the _get_formatted_string(strFormat) member function: you pass a format string to it, and it returns the formatted string. For example, if you pass {DD}/{MM}, it will return something like 15/09.

The sole purpose of format_obj(value, strFormat) function is to allow calling the value’s _get_formatted_string function.
  • ·        The function creates a format_obj_t temporary object, which can be written to any stream. It has an overloaded operator <<.
  • ·        When the function writes to a stream, it calls value._get_formatted_string, passing the format string.

We can do better: Even more user-friendly
When defining your format map, you provide a description for each formatter ID (you might want to recheck Listing A). Besides making it easier for you to maintain the formatter map, the description can also be shown to the user.

Every class derived from obj_formatter provides access to all of its available formatters. The function your_class::get_available_formatters() returns an STL array (formatter_descriptions_array) of formatter_description objects. Given a formatter_description, you can use the following:
  • ·        get_formatter returns the formatter ID.
  • ·        get_description returns the formatter’s description.
  • ·        set_description allows you to localize the description.

By showing the available formatters to users, you can allow them to customize how they want to see values of this type. Showing the formatters is as easy as possible, since formatter_description is derived from obj_formatter as well. Use:
  • ·        {name} to identify the formatter ID.
  • ·        {desc} to identify the formatter description.
  • ·        {sample}, as explained below.

Listing D shows how easy it is to show the available formatters to the user. As a bonus, you can provide a sample to show the user what each formatter does, in a simple and consistent way. By writing {sample} internally, you will apply the formatter on the given sample and print the result.

Providing a sample is easy. In your_class, just provide the following function:
static your_class * get_format_sample()

Show me how it works
You can download the source code for the example here. Listing E (in the download) contains the full implementation of the time formatter class. Since you might use your proprietary DateTime class, creating one more DateTime class would definitely not help. That’s why I took another approach.
  • ·        Deal only with time_t variables. (If you have another proprietary DateTime class, it should be able to be converted to a time_t.)
  • ·        Wrap the time_t in a format_time class at the time of writing.
  • ·        Apply the format_obj function.

Listing F (in the download) shows the Name class, which contains a First and Last Name. As you’ll see, there are many ways to show a name.

Run both examples, and check out the:
  • ·        Formatter maps. Both classes provide lots of way to format values.
  • ·        get_format_sample() function. Both classes provide one.
  • ·        Outputted available formatters.
  • ·        Outputted variables.

Finally, Listing G (in the download) shows the code for the object formatter (obj_formatter.h).

Use the object formatter to make your application even more user-friendly. Writing messages will be much more straightforward, and the code will become a lot clearer and easier to maintain.





Editor's Picks