Multiple Streams Going Nowhere

Multiple Streams Going Nowhere

By Paul Grenyer

Overload, 13(65):, February 2005


In this case study I am going to describe two streams I developed for use within my C++ testing framework, Aeryn [ Aeryn ]. Aeryn has two output streams. One is minimal and only reports test failures and the test, pass and failure counts. The other is more verbose and includes all the output from the minimal stream, plus a list of all test sets along with their individual test cases. The minimal stream is intended to be sent to the console and the verbose stream to a more permanent medium such as a log file or database, but either can be sent to any sort of output stream.

The use of the two streams introduces two specific problems:

  1. The stream sink for both streams must be passed into the function that runs the tests. For example:

    std::ofstream verbose("testlog.txt");
    std::stringstream minimal;
    testRunner.Run(verbose, minimal);
    

    Even if only one of the two outputs is required, both streams must be specified.

  2. The same information must be sent to both streams, which results in duplicate code. For example:

    verbose << "Ran 6 tests, 3 passes, 3 failures";
    minimal << "Ran 6 tests, 3 passes, 3 failures";
    

    This is far from ideal as every time the text sent to one stream is modified, the text sent to the other stream must also be modified. It would be all too easy to forget to update one or other of the streams or to update one incorrectly.

Both of these problems can be solved by writing a custom stream. Writing custom streams is covered in detail in section 13.13.3 (User-Defined Stream Buffers) of The C++ Standard Library [ Josuttis ]. As Josuttis does such a good job of describing custom streams and his book is widely distributed, I will only cover the necessary points relevant to this case study.

Null Output Stream

Problem 1 can be easily solved with a null output stream. A null output stream is a type of null object [ Null_Object ]. Kevlin Henney describes a null object as follows: "The intent of a null object is to encapsulate the absence of an object by providing a substitutable alternative that offers suitable default do nothing behaviour." So basically a null output stream is a stream that does nothing with what is streamed to it. Therefore if either the minimal or verbose stream is not required it can be directed to a null output stream. For example:

cnullostream ns;
testRunner.Run(ns, std::cout);

The key to writing a custom stream is implementing its stream buffer. The functionality for stream buffers is held in the standard library template class std::basic_streambuf . Custom stream buffers can be written by inheriting from std::basic_streambuf and overriding the necessary member functions.

It is not necessary for the custom stream buffer to be a template, but it makes life a lot easier if you want your custom stream to work with char , wchar_t and custom character traits. This is also discussed in detail in The C++ Standard Library.

template<typename char_type, typename traits
               = std::char_traits<char_type> >
class nulloutbuf : public
     std::basic_streambuf<char_type, traits> {
protected:
  virtual int_type overflow(int_type c) {
    return traits::not_eof(c);
  }
};

The code above shows the complete implementation for the null output stream buffer. The overflow member function is all that is needed to handle characters sent to the stream buffer. The traits::not_eof(c) function ensures that the correct character is returned if c is EOF .

Now that the stream buffer is complete it needs to be passed to an output stream. The easiest way to do this is to inherit from std::basic_ostream and have the stream buffer as a member of the subclass.

template<typename char_type, typename traits
               = std::char_traits<char_type> >
class null_ostream : public
       std::basic_ostream<char_type, traits> {
private:
  nulloutbuf<char_type, traits> buf_;

public:
  null_ostream()
    : std::basic_ostream<char_type,
                     traits>(&buf_), buf_() {}
};

Notice the constructor initialisation list. The buf_ member of null_ostream is passed to the basic_ostream base class before it has been initialised. In his book Josuttis actually puts buf_ first in the list, but this makes no difference. The base class is still initialised before buf_ . This could give rise to a problem where buf_ is accessed by a nullstream base class prior to it being initialised.

Some standard library implementations do nothing to avoid this and they don't need to. A library vendor knows their own implementation and if protection was required it would be provided. As the C++ standard gives no guarantee it is sensible for a custom stream to take steps to avoid the stream buffer being accessed before it is created. One way to do this is to put it in a private base class, which is then initialised before basic_ostream :

template<typename char_type, typename traits>
class nulloutbuf_init {
private:
  nulloutbuf<char_type, traits> buf_;

public:
  nulloutbuf<char_type, traits>* buf() {
    return &buf_;
  }
};
template<typename char_type, typename traits
               = std::char_traits<char_type> >
class nullostream : private virtual
       nulloutbuf_init<char_type, traits>,
     public
       std::basic_ostream<char_type, traits> {
private:
  typedef nulloutbuf_init<char_type, traits>
                              nulloutbuf_init;

public:
  nullostream() : nulloutbuf_init(), 
          std::basic_ostream<char_type,
            traits>(nulloutbuf_init::buf()) {}
};

The code above shows that as well as being inherited privately, nulloutbuf_init is also inherited virtually. This makes sure that nulloutbuf and nulloutbuf_init are initialised first, avoiding the undefined behaviour described in 27.4.4/2 of the [ CppStandard ]. The undefined behaviour would occur if nulloutbuf 's constructor was to throw in between the construction of basic_ios (a base class of basic_ostream ) and the call to basic_ios::init() from basic_ostream 's contrustor. See the C++ standard for more details.

Now that the implementation of null_ostream is complete two helpful typedefs can be added. One for char and one for wchar_t :

typedef null_ostream<char> cnullostream;
typedef null_ostream<wchar_t> wnullostream;

I always like to unit test the code I write and usually the tests are in place beforehand. Naturally I use Aeryn for unit testing. Testing null_ostream has its own interesting problems. I started by writing two simple tests to make sure that cnullostream and wnullostream compile and accept input:

void CharNullOStreamTest() {
  cnullostream ns;
  ns << "Hello, World!" << '!' << std::endl;
}

void WideNullOStreamTest() {
  wnullostream wns;
  wns << L"Hello, World!" << '!' << std::endl;
}

The whole point of a null output stream is that it shouldn't allocate memory when something is streamed to it; otherwise something like a std::stringstream could be used instead. Wanting to test for memory allocation caused me to write, with considerable help from accu-general members, a memory observer library, called Elephant (see sidebar) [ Elephant ]. Elephant allows me to write an observer ( NewDetector ) which can detect allocations from within null_ostream 's header file, which in this case, also holds its definition. Originally the observer was intended to monitor all allocations that occurred while using null_ostream , but as the standard permits stream base classes to allocate memory to store the current locale, I restricted the observer to allocations from null_ostream itself:

class NewDetector : public elephant::IMemoryObserver {
private:
  bool memoryAllocated_;

public:
  NewDetector() 
    : memoryAllocated_(false) {
  }

  virtual void OnAllocate(void*, std::size_t,
                          std::size_t,
                          const char* file) {
    // Crude black list.
    if(std::strcmp(file,
                   pg::null_ostream_header)) {
      memoryAllocated_ = true;
    }
  }

  virtual void OnFree(void*) {}

  bool AllocationsOccurred() const {
    return memoryAllocated_;
  }
};

In order to get OnAllocate to be called by the Elephant operator new overload that includes the name of the file it was called from, a macro must be introduced into null_ostream 's definition. The easiest way to do this is to wrap null_ostream 's header file with the macro in the test source file:

// nullostreamtest.h
#define new ELEPHANTNEW
#include "nullostream.h"
#undef new

Elephant: C++ Memory Observer

A full discussion of the design of Elephant is beyond the scope of this case study, but the principles on which it is based are simple and easy to explain. Elephant consists of two main components:

new / delete overloads:

Elephant has a total of eight pairs of new / delete overloads. As well as allocating and freeing memory, each overload registers its invocation with the memory monitor by passing the address of the memory that has been allocated or freed. Four of the eight new overloads also pass the line and file from which new was invoked.

Memory monitor:

Calls to the new / delete overloads are monitored by the memory monitor. The memory monitor is observer-compatible and users of Elephant can write custom observers (or use those provided) and register them with the memory monitor. Every time memory is allocated or freed via the new / delete overloads each observer is notified and passed the memory address and, where available, the line and file from which new was invoked.

In order to make sure that OnAllocate only registers allocations from null_ostream , a variable must be introduced into null_ostream 's header file:

const char* const null_ostream_header
                                  = __FILE__;

An ideal solution would not require the null_ostream header to be modified at all for testing. However I could not find a satisfactory alternative. Suggestions will be gratefully received.

Moving CharNullOStreamTest and WideNullOStreamTest into a class, and giving them new names to better represent what they now test for, allows NewDetector to be added as a member, and using Aeryn's incarnate function allows a new instance to be created for each test function call.

class NullOStreamTest {
private:
  NewDetector newDetector_;

public:
  NullOStreamTest()
    : newDetector_() {
    using namespace elephant;
    MemoryMonitorHolder().Instance().
                AddObserver(&newDetector_);
  }

  ~NullOStreamTest() {
    using namespace elephant;
    MemoryMonitorHolder().Instance().
                RemoveObserver(&newDetector_);
  }

  void NoMemoryAllocatedTest() {
    cnullostream ns;
    ns << testString << testChar
       << std::endl;
    IS_FALSE(
          newDetector_.AllocationsOccurred());
  }

  void NoMemoryAllocatedWideTest() {
    wnullostream wns;
    wns << wtestString << wtestChar
        << std::endl;
    IS_FALSE(
          newDetector_.AllocationsOccurred());
  }
};

Multi Output Stream

Problem 2 can be solved with what I have called a multi output stream. A multi output stream forwards anything that is streamed to it onto any number of other output streams. To solve the problem faced by Aeryn the multi output stream could simply hold references to two streams (one verbose, one minimal) as members, but this could potentially restrict future use when more than two streams may be required.

Again, the key is the output buffer. The first element to consider is how the multiple output streams, or at least some sort of reference to them, will be stored and how they will be added to and removed from the store. The easiest way to store the output streams is in a vector of basic_ostream pointers.

The original design for the multi output stream I came up with managed the lifetime of the output streams as well. This involved the output streams being created on the heap and managed by a vector of smart pointers. Therefore a smart pointer either had to be written or a dependency on a library such as boost [ boost ] introduced. As the lifetime of the multi output stream would be the same or very similar to the lifetime of the output streams there was really no need.

The easiest way to add and remove output streams is by way of an add function and a remove function. This functionality is shown in the code below.

template<typename char_type, typename traits
               = std::char_traits<char_type> >
class multioutbuf : public
               std::basic_streambuf<char_type,
                                    traits> {
private:
  typedef std::vector<std::basic_ostream<
                         char_type, traits>* >
                         stream_container;
  typedef typename stream_container::iterator
                         iterator;
  stream_container streams_;

public:
  void add(std::basic_ostream<char_type,
                              traits>& str) {
    streams_.push_back(&str);
  }

  void remove(std::basic_ostream<char_type,
                               traits>& str) {
    iterator pos = std::find(streams_.begin(),
                        streams_.end(), &str);

    if(pos != streams_.end()) {
      streams_.erase(pos);
    }
  }
};

The add function simply adds a pointer to the specified output stream to the store. The remove function must first check that a pointer to the specified output stream exists in the store, before removing it.

Josuttis describes the std::basic_streambuf virtual functions that should be overridden in a custom output buffer: overflow for writing single characters and xsputn for efficient writing of multiple characters.

template<typename char_type, typename traits
               = std::char_traits<char_type> >
class multioutbuf : public
               std::basic_streambuf<char_type,
                                    traits> {
  ...
protected:
  virtual std::streamsize xsputn(
                    const char_type* sequence,
                    std::streamsize num) {
    iterator current = streams_.begin();
    iterator end = streams_.end();

    for(; current != end; ++current) {
      (*current)->write(sequence, num);
    }

    return num;
  }

  virtual int_type overflow(int_type c) {
    iterator current = streams_.begin();
    iterator end = streams_.end();

    for(; current != end; ++current) {
      (*current)->put(c);
    }

    return c;
  }
};

A different approach would be to write three function objects and use for_each to call the appropriate function for each output stream in the store. However, this would not add a lot to the clarity and would not provide any better performance, but would create a lot of extra code.

The output buffer must be initialised and passed to an output stream and the output stream needs to have corresponding add and remove functions that forward to the output buffer's functions:

template<typename char_type, typename traits>
class multioutbuf_init {
private:
  multioutbuf<char_type, traits> buf_;

public:
  multioutbuf<char_type, traits>* buf() {
    return &buf_;
  }
};

template<typename char_type, typename traits
               = std::char_traits<char_type> >
class multiostream : private
       multioutbuf_init<char_type, traits>, 
      public
       std::basic_ostream<char_type, traits> {
private:
  typedef multioutbuf_init<char_type, traits>
                             multioutbuf_init;

public:
  multiostream() : multioutbuf_init(), 
         std::basic_ostream<char_type,
           traits>(multioutbuf_init::buf()) {}
  bool add(std::basic_ostream<char_type,
                              traits>& str) {
    return multioutbuf_init::buf()
                              ->add(str);
  }

  bool remove(std::basic_ostream<char_type,
                              traits>& str) {
    return multioutbuf_init::buf()
                              ->remove(str);
  }
};

All that remains is to provide two convenient typedefs, one for char and one for wchar_t :

typedef multi_ostream<char> cmultiostream;
typedef multi_ostream<wchar_t> wmultiostream;

The multi output stream is quite easy to test and should be tested for the following things:

  1. Output streams can be added to the multi output stream.

  2. All added output streams receive what is sent to the multi output stream.

  3. Streams can be removed from the multi output stream.

Although this looks likes three separate tests they are all linked and the easiest thing to do is to write a single test for char :

void CharMultiOStreamTest() {
  std::stringstream os1;
  std::stringstream os2;

  cmultiostream ms;
  ms.add(os1);
  ms.add(os2);
  ms << "Hello, World";

  IS_EQUAL(os1.str(), "Hello, World");
  IS_EQUAL(os2.str(), "Hello, World");

  ms.remove(os1);
  ms << '!'

  IS_EQUAL(os1.str(), "Hello, World");
  IS_EQUAL(os2.str(), "Hello, World!");
}

and a single test for wchar_t :

void WideMultiOStreamTest() {
  std::wstringstream wos1;
  std::wstringstream wos2;

  wmultiostream wms;
  wms.add(wos1);
  wms.add(wos2);
  wms << L"Hello, World";
  IS_EQUAL(wos1.str(), L"Hello, World");
  IS_EQUAL(L"Hello, World", wos2.str());

  wms.remove(wos1);
  wms << '!';

  IS_EQUAL(L"Hello, World", wos1.str());
  IS_EQUAL(L"Hello, World!", wos2.str());
}

Conclusion

Streams are a hugely powerful part of the C++ language; which few people seem to make use of and even fewer people customise for their own uses. The null_ostream and multi_ostream are very simple examples of customisation and I have shown here just how easy stream customisation is.

Acknowledgements

Thank you to Jez Higgins, Alan Stokes, Phil Bass, Alan Griffiths, Alisdair Meredith and Kevlin Henney for their comments at various stages of this case study.

References

[Aeryn] Aeryn: C++ Testing Framework. http://www.paulgrenyer.co.uk/aeryn/

[Josuttis] Nicolai M. Josuttis, The C++ Standard Library , Addison-Wesley, ISBN: 0-201-37926-0.

[Null_Object] Kevlin Henney. Null Object, Something for Nothing, http://www.two-sdg.demon.co.uk/curbralan/ papers/europlop/NullObject.pdf

[CppStandard] The C++ Standard , John Wiley and Sons Ltd. ISBN: 0-470-84674-7.

[Elephant] Elephant: C++ Memory Observer: http://www.paulgrenyer.dyndns.org/elephant/

[Boost] Boost. http://www.boost.org/






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.