...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
(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 | |
---|---|
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