Sometimes you want to vary a function or a type’s behavior based on a static parameter. In the cpp-netlib there are a number of things you might want to change based on some such parameter – like what the underlying string type should be and how large a buffer should be, among other things. The primary way to define this in a header-only manner is to use tag-based metafunctions.
The skeleton of the approach is based on a similar technique for defining type traits. In the cpp-netlib however the type traits are defined on opaque tag types which serve to associate results to a family of metafunctions.
To illustrate this point, let’s define a tag default_ which we use to denote the default implementation of a certain type foo. For instance we decide that the default string type we will use for default_ tagged foo specializations will be an std::string.
In the cpp-netlib this is done by defining a string metafunction type that is specialized on the tag default_ whose nested type result is the type std::string. In code this would translate to:
template <class Tag>
struct string {
typedef void type;
};
struct default_;
template <>
struct string<default_> {
typedef std::string type;
};
Starting with version 0.7, the tag dispatch mechanism changed slightly to use Boost.MPL. The idea is still the same, although we can get a little smarter than just using template specializations. Instead of just defining an opaque type default_, we use the Boost.MPL equivalent of a vector to define which root types of properties this default_ tag supports. The idea is to make the opaque type default_ inherit property tags which the library supports internally as definite extension points.
Our definition of the default_ tag will then look something like the following:
typedef mpl::vector<default_string> default_tags;
template <class Tag>
struct components;
typedef mpl::inherit_linearly<
default_tags,
mpl::inherit<mpl::placeholders::_1, mpl::placeholders::_2>
>::type default_;
template <class Tag>
struct components<default_> {
typedef default_tags type;
};
In the above listing, default_string is what we call a “root” tag which is meant to be combined with other “root” tags to form composite tags. In this case our composite tag is the tag default_. There are a number of these “root” tags that cpp-netlib provides. These are in the namespace boost::network::tags and are defined in boost/network/tags.hpp.
Using this technique we change slightly our definition of the string metafunction class into this:
template <class Tag>
struct unsupported_tag;
template <class Tag>
struct string :
mpl::if_<
is_base_of<
tags::default_string,
Tag
>,
std::string,
unsupported_tag<Tag>
>
{};
Notice that we don’t have the typedef for type in the body of string anymore, but we do inherit from mpl::if_. Since mpl::if_ is a template metafunction itself, it contains a definition of the resulting type which string inherits.
You can see the real definition of the string metafunction in boost/network/traits/string.hpp.
Once we have the defined tag, we can then use this in the definition of our types. In the definition of the type foo we use this type function string and pass the tag type parameter to determine what to use as the string type in the context of the type foo. In code this would translate into:
template <class Tag>
struct foo {
typedef typename string<Tag>::type string_type;
// .. use string_type where you need a string.
};
Using this approach we can support different types of strings for different tags on the type foo. In case we want to use a different type of string for the tag default_ we only change the composition of the string_tags MPL vector. For example, in cpp-netlib there is a root tag default_wstring which causes the string metafunction to define std::wstring as the resulting type.
The approach also allows for the control of the structure and features of types like foo based on the specialization of the tag. Whole type function families can be defined on tags where they are supported and ignored in cases where they are not.
To illustrate let’s define a new tag swappable. Given the above definition of foo, we want to make the swappable-tagged foo define a swap function that extends the original default_-tagged foo. In code this would look like:
struct swappable;
template <>
struct foo<swappable> : foo<default_> {
void swap(foo<swappable> & other) {
// ...
}
};
We also for example want to enable an ADL-reachable swap function:
struct swappable;
inline
void swap(foo<swappable> & left, foo<swappable> & right) {
left.swap(right);
}
Overall what the tag-based definition approach allows is for static definition of extension points that ensures type-safety and invariants. This keeps the whole extension mechanism static and yet flexible.