Version 1 Implementation: Test-Driven Development
I implemented the first version of format_iterator using test-driven development principles, in test-first mode. I confess that I anticipated that I would be able to write an article about its development, and implementing in this way would allow me to explain output iterator implementation by example, relying largely on the code to explain. So, that's what I'll do.
Assume any one of the uses of the iterator shown above. Compiling the code gives lots of errors about format_iterator not existing. Listing 1 shows the first iteration, the class skeleton that defines the appropriate member types for an output iterator. As explained in detail in Extended STL, Volume 1, an output iterator should have an iterator_category of std::output_iterator_tag, and all other members (difference_type, pointer, reference, and value_type) should be void; the type generator std::iterator is used to define them.
#include <stlsoft/util/std/iterator_helper.hpp> namespace fastformat { class format_iterator : public std::iterator<std::output_iterator_tag, void, void, void, void> { }; } /* namespace fastformat */
Listing 2 shows the next iteration, which changes the class name to format_output_iterator; format_iterator is now a creator function (a la std::make_pair()). This is because the iterator class will need to be a template, and using a creator function means that the compiler will automatically deduce the specialising types, saving us from having to explicitly specify them.
template <typename S, typename F> class format_output_iterator : public std::iterator<std::output_iterator_tag, void, void, void, void> { public: typedef format_output_iterator<S, F> class_type; public: format_output_iterator(S& sink, F const& format); }; template <typename S, typename F> inline format_output_iterator<S, F> format_iterator(S& sink, F const& format) { return format_output_iterator<S, F>(sink, format); }
Next, the compiler will complain that format_output_iterator<> does not support the dereference and increment operators. Listing 3 shows the addition of these in the next iteration. Note the use of the Dereference Proxy pattern, which proscribes fatuous (and non-portable) syntactic misuse of output iterator instances.
template <typename S, typename F> class format_output_iterator : public std::iterator<std::output_iterator_tag, void, void, void, void> { public: typedef format_output_iterator<S, F> class_type; private: class deref_proxy; friend class deref_proxy; public: format_output_iterator(S& sink, F const& format); private: class deref_proxy { public: deref_proxy(format_output_iterator* it) : m_it(it) {} private: format_output_iterator* const m_it; }; public: deref_proxy operator *() { return deref_proxy(this); } class_type& operator ++() { return *this; } class_type operator ++(int) { return *this; } };
Now the compiler will complain that the format_output_iterator<>::deref_proxy type does not support the assignment operator, leading us to Listing 4.
template <typename S, typename F> class format_output_iterator : . . . { . . . private: class deref_proxy { public: deref_proxy(format_output_iterator* it); public: template <typename A> void operator =(A const& value); private: void operator =(deref_proxy const&); . . . }; public: deref_proxy operator *() { return deref_proxy(this); } . . . };
Now we get a linker problem, because the dereference proxy type's assignment operator template is not defined. We implement it in terms of the new invoke_() method of the iterator class template (see Listing 5). Hopefully, in this step you can see how the format iterator works: it's implemented in terms of FastFormat's Format API function fmt() [4].
template <typename S, typename F> class format_output_iterator : . . . { . . . private: class deref_proxy { public: deref_proxy(format_output_iterator* it); public: template <typename A> void operator =(A const& value) { m_it->invoke_(value); } private: void operator =(deref_proxy const&); private: format_output_iterator* const m_it; }; template <typename A> void invoke_(A const& value) { fmt(m_sink, m_format, value); } public: deref_proxy operator *() { return deref_proxy(this); } . . . };
Naturally, the compiler now complains that it does not know what m_sink and m_format are. These members are defined in Listing 6.
template <typename S, typename F> class format_output_iterator : . . . { public: typedef format_output_iterator<S, F> class_type; typedef S sink_type; typedef F format_type; private: typedef std::basic_string<ff_char_t> string_type_; private: class deref_proxy; friend class deref_proxy; public: format_output_iterator(sink_type& sink, format_type const& format) : m_sink(sink) , m_format(::stlsoft::c_str_data(format), ::stlsoft::c_str_len(format)) {} private: class deref_proxy { . . . }; template <typename A> void invoke_(A const& value); public: deref_proxy operator *(); class_type& operator ++(); class_type& operator ++(int); private: // Fields sink_type& m_sink; string_type_ const m_format; };
Believe it or not, this series of steps was carried out in just a couple of hours. I've found this to be one of the effects of a test-driven approach. In suitable cases it can result in very fast development times. (This may be because, being guided by the objective tests, I have much more confidence, as well as making fewer missteps.) Of course, I had the guidance of the relevant chapters of Extended STL to help. (Not to mention the obvious advantage of having written it, of course!)