Boost C++ Libraries

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb Sutter and Andrei Alexandrescu, C++ Coding Standards

PrevUpHomeNext

Concepts in Depth

Defining Custom Concepts
Overloading
Concept Maps
Associated Types

(For the source of the examples in this section see custom.cpp)

Earlier, we used BOOST_TYPE_ERASURE_MEMBER to define a concept for containers that support push_back. Sometimes this interface isn't flexible enough, however. The library also provides a lower level interface that gives full control of the behavior. Let's take a look at what we would need in order to define has_push_back. First, we need to define the has_push_back template itself. We'll give it two template parameters, one for the container and one for the element type. This template must have a static member function called apply which is used to execute the operation.

template<class C, class T>
struct has_push_back
{
    static void apply(C& cont, const T& arg) { cont.push_back(arg); }
};

Now, we can use this in an any using call to dispatch the operation.

std::vector<int> vec;
any<has_push_back<_self, int>, _self&> c(vec);
int i = 10;
call(has_push_back<_self, int>(), c, i);
// vec is [10].

Our second task is to customize any so that we can call c.push_back(10). We do this by specializing concept_interface. The first argument is has_push_back, since we want to inject a member into every any that uses the has_push_back concept. The second argument, Base, is used by the library to chain multiple uses of concept_interface together. We have to inherit from it publicly. Base is also used to get access to the full any type. The third argument is the placeholder that represents this any. If someone used push_back<_c, _b>, we only want to insert a push_back member in the container, not the value type. Thus, the third argument is the container placeholder.

When we define push_back the argument type uses the metafunction as_param. This is just to handle the case where T is a placeholder. If T is not a placeholder, then the metafunction just returns its argument, const T&, unchanged.

namespace boost {
namespace type_erasure {
template<class C, class T, class Base>
struct concept_interface<has_push_back<C, T>, Base, C> : Base
{
    void push_back(typename as_param<Base, const T&>::type arg)
    { call(has_push_back<C, T>(), *this, arg); }
};
}
}

Our example now becomes

std::vector<int> vec;
any<has_push_back<_self, int>, _self&> c(vec);
c.push_back(10);

which is what we want.

(For the source of the examples in this section see overload.cpp)

concept_interface allows us to inject arbitrary declarations into an any. This is very flexible, but there are some pitfalls to watch out for. Sometimes we want to use the same concept several times with different parameters. Specializing concept_interface in a way that handles overloads correctly is a bit tricky. Given a concept foo, we'd like the following to work:

any<
    mpl::vector<
        foo<_self, int>,
        foo<_self, double>,
        copy_constructible<>
    >
> x = ...;
x.foo(1);   // calls foo(int)
x.foo(1.0); // calls foo(double)

Because concept_interface creates a linear inheritance chain, without some extra work, one overload of foo will hide the other.

Here are the techniques that I found work reliably.

For member functions I couldn't find a way to avoid using two specializations.

template<class T, class U>
struct foo
{
    static void apply(T& t, const U& u) { t.foo(u); }
};

namespace boost {
namespace type_erasure {

template<class T, class U, class Base, class Enable>
struct concept_interface< ::foo<T, U>, Base, T, Enable> : Base
{
    typedef void _fun_defined;
    void foo(typename as_param<Base, const U&>::type arg)
    {
        call(::foo<T, U>(), *this, arg);
    }
};

template<class T, class U, class Base>
struct concept_interface< ::foo<T, U>, Base, T, typename Base::_fun_defined> : Base
{
    using Base::foo;
    void foo(typename as_param<Base, const U&>::type arg)
    {
        call(::foo<T, U>(), *this, arg);
    }
};

}
}

This uses SFINAE to detect whether a using declaration is needed. Note that the fourth argument of concept_interface is a dummy parameter which is always void and is intended to be used for SFINAE. Another solution to the problem that I've used in the past is to inject a dummy declaration of fun and always put in a using declaration. This is an inferior solution for several reasons. It requires an extra interface to add the dummy overload. It also means that fun is always overloaded, even if the user only asked for one overload. This makes it harder to take the address of fun.

Note that while using SFINAE requires some code to be duplicated, the amount of code that has to be duplicated is relatively small, since the implementation of concept_interface is usually a one liner. It's a bit annoying, but I believe it's an acceptable cost in lieu of a better solution.

For free functions you can use inline friends.

template<class T, class U>
struct bar_concept
{
    static void apply(T& t, const U& u) { bar(t, u); }
};

namespace boost {
namespace type_erasure {

template<class T, class U, class Base>
struct concept_interface< ::bar_concept<T, U>, Base, T> : Base
{
    friend void bar(typename derived<Base>::type& t, typename as_param<Base, const U&>::type u)
    {
        call(::bar_concept<T, U>(), t, u);
    }
};

template<class T, class U, class Base>
struct concept_interface< ::bar_concept<T, U>, Base, U, typename boost::disable_if<is_placeholder<T> >::type> : Base
{
    using Base::bar;
    friend void bar(T& t, const typename derived<Base>::type& u)
    {
        call(::bar_concept<T, U>(), t, u);
    }
};

}
}

Basically we have to specialize concept_interface once for each argument to make sure that an overload is injected into the first argument that's a placeholder. As you might have noticed, the argument types are a bit tricky. In the first specialization, the first argument uses derived instead of as_param. The reason for this is that if we used as_param, then we could end up violating the one definition rule by defining the same function twice. Similarly, we use SFINAE in the second specialization to make sure that bar is only defined once when both arguments are placeholders. It's possible to merge the two specializations with a bit of metaprogramming, but unless you have a lot of arguments, it's probably not worth while.

(For the source of the examples in this section see concept_map.cpp)

Sometimes it is useful to non-intrusively adapt a type to model a concept. For example, suppose that we want to make std::type_info model less_than_comparable. To do this, we simply specialize the concept definition.

namespace boost {
namespace type_erasure {

template<>
struct less_than_comparable<std::type_info>
{
    static bool apply(const std::type_info& lhs, const std::type_info& rhs)
    { return lhs.before(rhs) != 0; }
};

}
}

[Note] Note

Most, but not all of the builtin concepts can be specialized. Constructors, destructors, and RTTI need special treatment from the library and cannot be specialized. Only primitive concepts can be specialized, so the iterator concepts are also out.

(For the source of the examples in this section see associated.cpp)

Associated types such as typename T::value_type or typename std::iterator_traits<T>::reference are quite common in template programming. Boost.TypeErasure handles them using the deduced template. deduced is just like an ordinary placeholder, except that the type that it binds to is determined by calling a metafunction and does not need to be specified explicitly.

For example, we can define a concept for holding an iterator, raw pointer, or smart pointer as follows. First, we define a metafunction called pointee defining the associated type.

template<class T>
struct pointee
{
    typedef typename mpl::eval_if<is_placeholder<T>,
        mpl::identity<void>,
        boost::pointee<T>
    >::type type;
};

Note that we can't just use boost::pointee, because this metafunction needs to be safe to instantiate with placeholders. It doesn't matter what it returns as long as it doesn't give an error. (The library never tries to instantiate it with a placeholder, but argument dependent lookup can cause spurious instantiations.)

template<class T = _self>
struct pointer :
    mpl::vector<
        copy_constructible<T>,
        dereferenceable<deduced<pointee<T> >&, T>
    >
{
    // provide a typedef for convenience
    typedef deduced<pointee<T> > element_type;
};

Now the Concept of x uses two placeholders, _self and pointer<>::element_type. When we construct x, with an int*, pointer<>::element_type is deduced as pointee<int*>::type which is int. Thus, dereferencing x returns an any that contains an int.

int i = 10;
any<
    mpl::vector<
        pointer<>,
        typeid_<pointer<>::element_type>
    >
> x(&i);
int j = any_cast<int>(*x); // j == i

Sometimes we want to require that the associated type be a specific type. This can be solved using the same_type concept. Here we create an any that can hold any pointer whose element type is int.

int i = 10;
any<
    mpl::vector<
        pointer<>,
        same_type<pointer<>::element_type, int>
    >
> x(&i);
std::cout << *x << std::endl; // prints 10

Using same_type like this effectively causes the library to replace all uses of pointer<>::element_type with int and validate that it is always bound to int. Thus, dereferencing x now returns an int.

same_type can also be used for two placeholders. This allows us to use a simple name instead of writing out an associated type over and over.

int i = 10;
any<
    mpl::vector<
        pointer<>,
        same_type<pointer<>::element_type, _a>,
        typeid_<_a>,
        copy_constructible<_a>,
        addable<_a>,
        ostreamable<std::ostream, _a>
    >
> x(&i);
std::cout << (*x + *x) << std::endl; // prints 20


PrevUpHomeNext