Boost logo

Boost :

From: Daniel Walker (daniel.j.walker_at_[hidden])
Date: 2008-05-15 18:25:08


On Thu, May 15, 2008 at 9:09 AM, Giovanni Piero Deretta
<gpderetta_at_[hidden]> wrote:
> On Thu, May 15, 2008 at 4:06 AM, Daniel Walker
> >
> I.e. you loose the ability to be polymorphic when you actually
> construct a variable of a polymorphic type, as you
> have to fix the type. If c++ didn't allow creating variables of type
> 'plus' and only allowed variables of type function<sig>, it would have
> a Rank-1 Polymorphic Type system.
>
> A variable of type 'plus' is instead 'Rank-n' polymorphic, I.e. it
> retains its polymorphic behavior no matter
> how many levels of nesting are applyed (i.e. you can pass it to an
> higher order function which in turn is passed to an higher order
> function and so on). Wikpedia definition doesn't make it clear:

I haven't read the Wiki you're citing, and this is my first exposure
to some of this terminology, but this seems like a good categorization
to me. However, I would say that C++ supports multiple polymorphic
types. function<sig> is rank-1 polymorphic, where as 'plus' is rank-n.
Another example, std::plus is rank-1, and the 'plus' from my example
is rank-n. Thanks for bringing this up; these seem like really useful
distinctions.

>> ... add_ints and add_floats are objects of two different types that
>> both refer to the same function - plus. To convert between them, what
>> is needed is a functional_cast.
>>
>> Now, boost::function already has infrastructure to support this via
>> its target() member function. However, it does not retain the
>> underlying polymorphic functor type. We need some protocol to
>> encode/communicate this type.
>
> I think this is in direct contrast with the primary reason of
> std::function (or any other
> type erasure wrapper), which is to erase the type of the wrapped
> function and only retain
> its behavior. I do not see how you can retain the type and still preserve it...

See, I think the primary utility of boost::function is that it allows
you to treat builtin functions and function objects more
interchangeable. You can assign either a function pointer or a
function object to a boost::function object as long as they have the
same call signature. It blurs the boundary between second and first
class functions.

I think the type erasure of wrapped function objects is a by product
of the fact that builtin functions have no type, at least not in the
sense of constructability; i.e. there are no values in C++ that are of
a function type.

Maybe our different view points stem from the notion of wrapping.
boost::function is a call-wrapper, which is to say it's purpose is to
deffer a function call (i.e. implement operator() to dispatch to a
previously specified function). In doing so, boost::function also
erases the type of the contained callable objects, and can be viewed
as a "type-wrapper." polymorphic_function is a call-wrapper, which
allows the user to specify whether or not to erase the type of
contained function objects.

>> By using "polymorphic" signatures as the target of the cast,
>> implementing functional_cast is trivial and allows you to do
>> conversions like so:
>>
>> function<int(int,int)> f0
>> = functional_cast<plus(int,int)>(add_floats);
>> function<float(float,float)> f1
>> = functional_cast<plus(float,float)>(add_ints);
>>
>
> Ok, but you need to know the function type (i.e. plus). So, what
> is the point of using std::function then?

The point is to treat arbitrary callable objects as first class; i.e.
using function you can guarantee that whether you're given a builtin
function or function object you will be able to work with first class
types and first class values. Now in a situation where you have more
information, and you know that the rank-1 polymorphic boost::function
is wrapping a rank-n polymorphic object, functional_cast allows you to
cast between each of the n "ranks" of the rank-n function object. Or
something to that effect. ;-)

>
> Note that in many cases where std::function is useful (for example
> to hold the result of a complex lambda or bind expression), you
> do not know the stored function type.

That's true. If you don't know the polymorphic type, you cannot use it
polymorphically. This has always been the case with boost::function,
and there's no change from the status quo here.

>> Now polymorphic_function can be used in place of boost::function to
>> wrap arbitrary callable objects without compromising polymorphism.
>>
>> // A function.
>> int f(int i) { return i; }
>>
>> // A polymorphic functor.
>> struct g {
>> template <class> struct result;
>> template <class T>
>> struct result<g(T)> {
>> typedef T type;
>> };
>> template<class T>
>> T operator()(T t) { return t; }
>> };
>>
>> // Treat call signatures the same as boost::function
>> polymorphic_function<int(int)> f0 = f;
>> polymorphic_function<int(int)> f1 = g();
>>
>> // Treat polymorphic signatures polymorphically
>> polymorphic_function<g(_1)> f2 = g();
>
> this of course works, but you are no longer erasing the type of g here!!! It
> is right there, encoded in polymorphic_function instantiation: I cannot use it
> to pass a PFO to a function in another translation unit,
> I cannot use it to store a lambda function because I do not know its type and I
> cannot build a type homogeneous container which stores wrappers to different
> PFOs!

You can use polymorphic_function (maybe it should be called
rankn_function) the same way as boost::function. So, if you want to
erase the type, instantiate it with a call signature the same as
boost::function. Then you can use it across translation units, store
lambda functions in it, all that.

But if you have a polymorphic object, and you'd like to maintain it's
polymorphism, you have to tell the compiler its type (so that it can
preform overload resolution on the operator()s at the call site).
boost::function doesn't allow you to make this trade-off.
polymorphic_function allows you to select between the two - rank-1 or
rank-n, to continue to use the proper terminology - by specifying
either call signatures or polymorphic signatures.

>
> What is the point of using a wrapper then? I do not think that there is
> a (non contrived) use case where a non erasing polymorphic_function
> object is more useful than using 'g' directly.

The point is to have a uniform way of treating functions as first
class, both builtins and function objects. polymorphic_function
handles all the same use cases as boost::function plus an additional
one. boost::function decouples a function call from the values at the
call site, but not the types, so it can only support rank-1
polymorphism. polymorphic_function also decouples the types at the
call site, when wrapping an object that can support rank-n
polymorphism.

Here's a concrete example that hopefully isn't too contrived, and also
goes back to the topic of this thread. The multi-signature function
must be loaded with at least one signature that exactly matches the
types at the call site (well, convertibly matches). If none exist,
there's an error. What if you wanted a fall-back overload? For
example, say you're making an overload_set to preform addition, and
you have a special, custom function for integers as well as my
polymorphic plus example from before. The polymorphic plus could serve
as a fallback for adding types other than int, but with an
overload_set implementation based on boost::function, the best you can
do is "demote" plus to numerous rank-1 objects.

int add_ints(int x, int y)
{
    // do something special
    return x + y;
}

overload_set<
    mpl::vector<
        int(int,int)
      , float(float,float)
      , complex(complex,complex)
        // .etc
>
> add(add_ints
       , plus
       , plus
         // and so on
);

If the overload_set were instead based on polymorphic_function, then
after loading add_int for integers, you only need to specify plus once
to handle all other types.

overload_set<
    mpl::vector<int(int,int), plus(_1,_1)>
> add(add_ints, plus);

This is possible because polymorphic_function provides a single
interface to wrap both builtins and function objects as rank-1 or
rank-n accordingly. Granted the magic is all in the "polymorphic"
signature. But polymorphic_function conveniently provides a single
interface for handling both signature protocols.

Great points! Thanks!

Daniel Walker


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk