Software

Easily write and format ranges and containers in C++

The C++ Standard Template Library (STL) offers a handy way to write values to streams, but it doesn't work with arrays. Here's a simple approach that lets you write containers and ranges to streams using familiar syntax.


By John Torjo

The Standard Template Library (STL) provides an elegant way to write values to STL streams. Every type that has overloaded operator << can be written to any stream. Unfortunately, this is not always the best solution, and it doesn’t work with arrays. In this article, I will show you a better way to provide a consistent interface for writing ranges and containers to streams, while still using the operator <<.

First, let’s see what a general solution should provide. It should:
  1. ·        Be able to write ranges and containers. The solution should work for STL-like ranges and then make it easy to write containers as well. Writing a range means that we can write C-arrays as well, by providing an [A, A + N] sequence (A is the array, N is the size of the array).
  2. ·        Be able to surround each element, if needed. For instance, you might want to write an array like this: ‘[John, James, Corina]’
  3. ·        Be able to apply a transformation to each element, if needed. For instance, you might want to write the employees names, with their last names in uppercase, like this: [DOE, John; KEITH James; DYLAN, Richard] Or, you might want to write an array of integers, using the absolute value of each element.
  4. ·        Have reasonable defaults for the two preceding points. For example, if we don’t apply a transformation, the default transformation should print the element as is (the identity transformation).

By combining these capabilities, you get a powerful way to write arrays. If you’re not yet convinced, check out the examples shown at the end of the article.

Implementing the solution
Let’s break up the solution described above into pieces and handle each one individually:
  • ·        To accomplish point 1, we will have two functions: range for a range and container for a container.
  • ·        To accomplish point 2, we will have a writer object.
  • ·        To accomplish point 3, we will have a transformation—a functor or function that takes two parameters: the stream to write to and the value to write. (Note: A functor is an object that behaves as a function; in other words, it has an overloaded operator <<). The function or functor will transform the value and write it to the stream.
  • ·        Point 4 will be accomplished by overloading. We will have a formatter function that, based on the parameters given, will provide defaults where needed.

We will also have several functions that return objects that write a range or container. For each of these objects, we have overloaded the operator <<, which will write the underlying range or container to the destination stream.

For example, the following code creates an object that can write the aNames container. The operator << will be applied to it, which in turn will write the aNames to the std::cout.
typedef std::list< std::string> StringsArray;
StringsArray aNames;
aNames.push_back( "John");
aNames.push_back( "James");
aNames.push_back( "Corina");
// will print the array of names
std::cout << container( aNames) << std::endl ;

Writing syntax
When writing a range you must follow this syntax:
range( itFirst, itLast [, formatter])

When writing a container, use this syntax, which internally calls the above:
container( cont, [, formatter])

This syntax allows writing both ranges and containers in a simple and straightforward way. The formatter decides:
  • ·        What transformation will be applied to each element.
  • ·        How the elements will be written (see below).

The formatter function is optional, and if not present, a default formatter will be used. It will apply no transformation to the elements and use a default writer. Table A presents a few ways to write an array with three elements.
Table A

Output

Type of writing

John, James, Corina

Default

{John}, {James}, {Corina}

With formatter (custom writer)

{[0] John}, {[1] James}, {[2] Corina}

With formatter (custom writer and custom transformation)

[0] John, [1] James, [2] Corina

With formatter (custom transformation)

[John]
[James]
[Corina]

With formatter (custom writer)

[0] John
[1] James
[2] Corina

With formatter (custom writer and custom transformation)

'[0] John'
'[1] James'
'[2] Corina'

With formatter (custom writer and custom transformation)

Alternate approaches to writing arrays

The writer
The writer determines how the surroundings of the elements are written. There are three surroundings:
  • ·        The prefix—What we write before all elements in the array
  • ·        The after element—What we write after each element (except for the last one)
  • ·        The suffix—What we write after all elements in the array

To format each of these surroundings, the writer classes need to provide three functions:
void write_prefix( streamOut)
void write_after_element( streamOut)
void write_suffix( streamOut)

You can create your own writer class, but the two already provided should suffice:
  • ·        basic_range_writer is the default writer. It writes no prefix, it writes a comma after each element, and the suffix is one space. It will write elements like this: John, James, Corina .
  • ·        range_writer lets you provide the prefix, after element, and suffix. For example, you can write elements like this: [John, James, Corina] or {John}, {James}, {Corina}.

The transformation
The transformation allows you to transform each element. As we said earlier, it can be either a functor or a function, and it takes two parameters: the stream to write to and the object to write. The default transformation, shown in Listing A, writes the element as is.

You can also create your own, more complex transformation. For instance, you might create a transformation that prefixes each element by its index, as shown in Listing B.

Using Listing B, you can write arrays like this:
  • ·        ‘[1] John, [2] James, [3] Corina’ (PrefixByIndex transformation)
  • ·        ‘{ [1] John and [2] James and [3] Corina }’ (PrefixByIndex transformation combined with a writer( “{ ”, “ and “, “ }”) )

The formatter objects
If you decide to provide a formatter when writing your range or container, you have several to choose from. The trick is that, based on the parameters you pass to the formatter function, you can create the correct object. The formatter functions will apply a transformation and/or use a writer to format the elements. Here are the available formatters:
  • ·        formatter( Function transformation) creates an object that applies the given transformation and the default writer (see basic_range_writer).
  • ·        formatter( Function transformation, strAfterElement) creates an object that applies the given transformation; the writer uses no prefix or suffix, only the given after element.
  • ·        formatter(strPrefix, strAfterElement, strSuffix) creates an object that applies the default transformation; the writer uses the given prefix, after element, and suffix.
  • ·        formatter( Function transformation, strPrefix, strAfterElement, strSuffix) creates an object that applies the given transformation; the writer uses the given prefix, after element, and suffix.

More extensive examples
Listing C shows what you can do by combining the transformation with the writer. The comments show the output of the code.

The end result
Finally, Listing D shows all the code that allows writing ranges and containers to streams, along with examples of its usage.

Conclusion
With the help of the above functions, you can easily write and format ranges and containers. In a future article, I will show you how to do the same for STL collections.

 

Editor's Picks

Free Newsletters, In your Inbox