Boost logo

Boost :

From: Eric Niebler (eric_at_[hidden])
Date: 2008-04-05 15:10:59


My experience developing Proto has taught me a hard lesson about the
ResultOf protocol that I thought I would share. Maybe someone here can
tell me if/where I've gone wrong.

There's a part of Proto (proto::call<>) that takes function objects,
passes arguments to them and returns the result. I use result_of to
compute the return type. Fine.

Proto defines a function object, make_foo, whose behavior depends on the
lvalue/rvalue-ness of its arguments. So for instance:

   // Return type of make_foo when passed a rvalue
   // int and an lvalue float:
   result_of< make_foo( int, float & ) >::type

Like a good citizen, I've written make_foo's function call operator to
recognize reference_wrapped and non-const-ref arguments as representing
lvalues and all others as rvalues. (This is because C++03 doesn't have
rvalue references.) Fine.

Now, given some function type F (which may or may not be make_foo) and
some arguments a0...an (which may or may not be lvalues), how do I invoke F?

The strictly correct thing to do would be to wrap lvalues in
reference_wrappers before passing them to F. If I don't, make_foo will
do the wrong thing. However, Proto needs to work with function objects
in the wild, very few of which do the right thing with reference_wrapped
arguments, in my experience. What to do? I don't know.

This business of needing to reference_wrap objects to get correct
lvalue/rvalue semantics is a deal-breaker for Proto transforms. If I
needed to wrap each lvalue argument before forwarding it to the next
transform, and unpack each argument within each tranform, it would:

a) Make writing transforms extremely tedious.
b) Kill compile times with lots of unnecessary mpl::if_,
    is_reference, reference_wrapper and unwrap_reference
    instantiations.

For these reasons, I've decided that Proto's transforms will use a
different function object protocol. None of that will be visible from
the outside. Transforms will still look like TR1-style function objects.
But transforms will provide a separate interface for internal
consumption that doesn't have this rvalue/lvalue problem.

The problem, as I see it, stems from the fact that the return type
calculation is separated from the function invocation.

struct blarg
{
   template<typename This, typename Arg>
   struct result< This( Arg ) >
   {
     // OK, if Arg is a reference, then it's an lvalue!
   };

   template<typename Arg>
   typename result< make_foo( ??? ) >::type
   operator()( Arg const & arg )
   {
     // whoops, I don't know here if arg is an lvalue or rvalue
   }
};

If function objects were instead written like this ...

struct blarg
{
   template<typename Arg>
   struct impl
   {
     // OK, if Arg is a reference, it's an lvalue
     typedef ... result_type;

     result_type operator()(typename add_const_ref<Arg>::type arg)
     {
       // The right thing can happen here.
     }
   };
};

... the problem goes away.

-- 
Eric Niebler
Boost Consulting
www.boost-consulting.com

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