Wiki

Clone wiki

opqit / Motivation

A motivating example

Let us suppose that we are creating a little "electronic cookbook" application to store and access various recipes.

We might have a class, cookbook, that looks something like this:

#ifndef COOKBOOK_HPP_
#define COOKBOOK_HPP_

#include <set>
#include <string>

#include <boost/iterator/transform_iterator.hpp>
#include <boost/iterator/filter_iterator.hpp>
#include <boost/function.hpp>

class recipe
{
    // ...
};

// A database of recipes.
class cookbook
{
    public:
        typedef std::set<recipe>::const_iterator const_iterator;

        const_iterator begin() const;
        const_iterator end() const;

        // ...

    private:
        std::set<recipe> recipes_;
};

Let's also suppose that we'd like to define a "view" that gives us a way to iterate over the quick and easy recipes:

// ...continued

struct is_quick_and_easy
{
    bool operator() (const recipe &r) const
    {
        return r.num_ingredients() <= 10 && r.cooking_time_minutes() <= 20;
    }
};

class quick_and_easy_view
{
    public:
        // Construct an iterator type that filters a sequence of recipes from a cookbook
        // and dereferences to give an std::string containing the contents of the recipe.
        typedef
        boost::transform_iterator
        <
            boost::function<std::string (const recipe &)>,
            boost::filter_iterator<is_quick_and_easy, cookbook::const_iterator>
        >
        const_iterator;

        quick_and_easy_view(const cookbook &cb) : cb_(cb) { }

        const_iterator begin() const;
        const_iterator end() const;

    private:
        const cookbook &cb_;
};

#endif // COOKBOOK_HPP_

We might use the code, once implemented, as follows:

void print_all_recipes(const cookbook &db)
{
    typedef cookbook::const_iterator iter_t;
    for (iter_t b = db.begin(), e = db.end(); b != e; ++b)
    {
        std::cout << b->to_string() << '\n';
    }
}

void print_quick_and_easy_recipes(const cookbook &db)
{
    typedef quick_and_easy_view::const_iterator iter_t;

    quick_and_easy_view qaev(db);
    for (iter_t b = qaev.begin(), e = qaev.end(); b != e; ++b)
    {
        std::cout << *b << '\n';
    }
}

But there’s a problem. While the design of the class seems reasonably sensible on the face of things, the cookbook.hpp header actually pulls in a huge number of external headers.

The boost headers alone bring in around 260 additional includes (checked against boost version 1.34.1). <set> may pull in a significant number, too. This is a dependencies nightmare. Any time one of those headers change, all of the code that #includes our header will have to be recompiled.

To avoid this nastiness, we rummage around in our toolbox and come across the pimpl idiom, which can be employed in a number of situations to hide the implementation of a class behind an opaque pointer. But alas, this doesn’t get us very far, here.

Why not? Well, the return type of the cookbook::begin() and cookbook::end() member functions need to be known at compile time and they’re defined in terms of std::set's iterators. So even if we did hide the recipes_ data member behind an opaque pointer, we’d still have to #include the <set> header in our own so that we get the definitions of the iterator types.

Similarly, the iterator types for the quick_and_easy_view require that all three of the boost headers stay where they are.

So it’s really the iterators that are causing us grief, here. This is where opqit can help. By using the opaque iterators it provides, we can re-write the header as follows:

#ifndef COOKBOOK_HPP_
#define COOKBOOK_HPP_

#include <string>
#include <opqit/opaque_iterator_fwd.hpp>

class recipe
{
    // ...
};

// A database of recipes.
class cookbook
{
    public:
        typedef opqit::opaque_iterator<const recipe, opqit::input> const_iterator;

        const_iterator begin() const;
        const_iterator end() const;

        // ...

    private:

        struct impl;
        impl *pimpl_;
};

class quick_and_easy_view
{
    public:
        typedef opqit::opaque_iterator<const std::string, opqit::input> const_iterator;

        quick_and_easy_view(const cookbook &cb);

        const_iterator begin() const;
        const_iterator end() const;

    private:
        const cookbook &cb_;
};

#endif // COOKBOOK_HPP_

So we've now got rid of all of the boost headers and <set> by using a pimpl in the cookbook class. But hold on a minute, what if <opqit/opaque_iterator_fwd.hpp> is just as nasty? Fortunately it isn't. In fact the entire contents of the header is as follows (with comments removed):

#ifndef OPAQUE_ITERATOR_FWD_HPP_0159_25052007
#define OPAQUE_ITERATOR_FWD_HPP_0159_25052007

namespace opqit
{
    template<typename Val, typename Tag>
    class opaque_iterator;

    struct output { };
    struct input { };
    struct forward { };
    struct bidir { };
    struct random { };

    template<typename Iter>
    struct supports_arrow
    {
        static const bool value = true;
    };

} // close namespace opqit

#endif // OPAQUE_ITERATOR_FWD_HPP_0159_25052007

So we have significantly reduced the number of headers indirectly included by cookbook.hpp, and thus its compile-time dependencies.

We can change the underlying recipe container from an std::set to something else such as std::vector without forcing a recompile of those source files that #include our header.

Updated