multiio - TextIO Derivative for multiple devices

Introduction

The MultiIO package provides a set of TextIO devices that support multiple input and output streams.

For example, suppose you want to create a log file that contains everything written to standard output. The solution would be to use a multiple output device connected to both standard output and a file device for the log file. Then, use the multiple output device for all output.

The MultiIO output device is parallel - everything written to the device is replicated to all attached devices. The input device however is sequential - each attached device is read in turn.

Output Devices

Usage

Multiple output devices route text output to any number of attached output devices. The same text is sent to all of the attached devices. There is in principle no limit to the number of devices that can be attached in this way, although in practice it is hard to imagine more than two being used. The most obvious use for the multiple output device is to produce a log file that contains a copy the text printed to standard output.

For example, consider the Error Handler component that can be used to produce text messages. It has the following interface:

class error_handler
{
public:
  ...
  error_handler(otext& device,unsigned limit = 0,bool show = true);
  ...
  bool information(const std::string& id) throw(id_error,format_error);
  ...
  bool warning(const std::string& id) throw(id_error,format_error);
  ...
  bool error(const std::string& id) throw(id_error,format_error,limit_error);
  ...
};

The constructor takes as its argument an otext object - this is the superclass of all output devices and this means that any output device can be used. Normally, this might be constructed using the standard error (or output) as the output device:

#include "error_handler.hpp"
#include "fileio.hpp"
...
error_handler errors(ferr);

The ferr object is a pre-defined instance of an output file device (class oftext, which is a subclass of otext) attached to standard error.

However, this approach doesn't create a log file. How can this be done? The error handler itself does not provide this functionality. Is the error handler disfunctional? Should I rewrite it to provide log-file support? Should I rewrite every other component to provide log-file support just in case I need it? This would be the traditional approach and its about as naff as a Microsoft product. The answer is so much simpler than this - create a multi-output device attached to both standard error and the log file. Then pass that multi-output device to the error handler as its output device. Here's the code:

#include "error_handler.hpp"
#include "fileio.hpp"
#include "multiio.hpp"
...
oftext log("messages.log");
omtext output(ferr,log);
error_handler errors(output);

The first line (excluding the includes) creates an output file device called log which is connected to the file messages.log. A multi-output device of class omtext and called output is then created which is connected to two devices - ferr (connected to standard error) and log. This multi-output device is then passed to the error handler.

The end result is that every line of text produced by the error handler will go both to standard error and to the log file.

The elegance of this solution is that this logging feature has been added by simply changing a couple of lines of code. The rest of the program needs no changes and no other part of the program needs to know that output is being logged.

Interface

The interface to the multi-output device class omtext is:

class omtext : public otext
{
public:
  omtext(void);
  omtext(const otext&);
  omtext(const otext&, const otext&);
  void open(const otext&);
  void open(const otext&, const otext&);
  unsigned add(const otext&);

  unsigned device_count(void) const;
  otext& device_get(unsigned);
  const otext& device_get(unsigned) const;
};

Note: this is a subclass of otext and so inherits all the functionality of its superclass.

The atomic operations for setting up a multi-output device are the first constructor and the add method. The constructor sets up a multi-output device with no attached devices. The add method can then be called any number of times to attach devices. The example above could be rewritten to use only these atomic operations:

#include "error_handler.hpp"
#include "fileio.hpp"
#include "multiio.hpp"
...
oftext log("messages.log");
omtext output;
output.add(ferr);
output.add(log);
error_handler errors(output);

The other functions are composite operations that provide convenient shortcuts. The two extra constructors create a multi-output device and add one or two attached devices respectively. The open methods close the device and recreate it with one or two attached devices respectively. Finally, the device_count and device_get methods provide access to the attached devices, should that ever prove necessary.

Devices are numbered from 0 to device_count()-1, using the normal C++ conventions for arrays. When a device is added, its index number is returned from the add function. It can be accessed by calling device_get using that index number. Alternatively, all the devices can be accessed by using a for loop:

for (unsigned i = 0; i < output.device_count(); i++)
{
  otext& device = output.device_get(i);
  ...
}

Input Devices

Usage

Multiple-input devices are somewhat different to multiple-output devices. They are sequential rather than parallel. This means that each inpout device is read in turn until it indicates end-of-file. Then the next device is read and so on, until all devices have indicated end-of-file. Only then does the MultiIO input device indicate end-of-file. The effect is to concatenate all of the input devices attached to the multiple-input device.

As this description suggests, the purpose of the multiple-input device is to concatenate different devices. This is not limited to file devices - it is possible to concatenate, say, a file with an internet connection. For example, a header file could be added before the start of a web page and a footer file could be added after the end of a web page. From the viewpoint of the program using the multiple-input device they would all appear concatenated as a single file.

The main limitation of this approach is that all devices attached to the multiplt-input device must be opened before they are attached. This could be a problem if there is a limit on the number of opened devices. For example, most operating systems limit the number of files that can be opened at once and most web servers limit the number of connections that can be opened from a single client at once. Nevertheless, the multiple-input device can occasionally be useful.

Here's an example of how to use a multiple-input device to concatenate the set of all files specified from the command line and print them on standard output:

#include "fileio.hpp"
#include "multiio.hpp"
...
int main (int argc, char* argv[])
{
  ...
  // create the muli-input device
  imtext input;
  // add all the input files to it
  for (unsigned i = 1; i < argc; i++)
  {
    iftext file(argv[i])
    input.add(file);
  }
  // pipe it all to the output
  fout << input;
  return 0;
}

There is no error handling in this example - if a file doesn't exist, it will appear to be just a zero-length file and no errors will be reported. You might want to add some error reporting to test whether a file opened successfully:

  for (unsigned i = 1; i < argc; i++)
  {
    iftext file(argv[i])
    if (file.error())
    {
       // report error
       ...
    }
    else
      input.add(file);
  }

Interface

The interface to the multi-input device class imtext is:

class imtext : public itext
{
public:
  imtext(void);
  imtext(const itext&);
  imtext(const itext&, const itext&);
  void open(const itext&);
  void open(const itext&, const itext&);
  unsigned add(const itext&);

  unsigned device_count(void) const;
  itext& device_get(unsigned);
  const itext& device_get(unsigned) const;
};

Note: this is a subclass of itext and so inherits all the functionality of its superclass.

This is very similar to the multi-output device, so the following is a cut'n'paste'n'edit of that description.

The atomic operations for setting up a multi-input device are the first constructor and the add method. The constructor sets up a multi-input device with no attached devices. The add method can then be called any number of times to attach ainput devices. The example above is in fact written to use only these atomic operations

The other functions are composite operations that provide convenient shortcuts. The two extra constructors create a multi-input device and add one or two attached devices respectively. The open methods close the device and recreate it with one or two attached devices respectively. Finally, the device_count and device_get methods provide access to the attached devices, should that ever prove necessary.

Devices are numbered from 0 to device_count()-1, using the normal C++ conventions for arrays. When a device is added, its index number is returned from the add function. It can be accessed by calling device_get using that index number. Alternatively, all the devices can be accessed by using a for loop:

for (unsigned i = 0; i < input.device_count(); i++)
{
  itext& device = input.device_get(i);
  ...
}