Boost logo

Boost :

From: Karl Nelson (kenelson_at_[hidden])
Date: 2000-12-06 15:09:37


[...]
> Let me just check we are talking about the same thing here, focusing on
> a straight zero arg version:
>
> template<typename result_type>
> class any_function
> {
> ...
> private:
> class body
> {
> public:
> virtual ~body_base();
> virtual result_type call() = 0;
> ... // cloning or counting features, as necess
> };
>
> template<typename adaptee_type>
> class adapter : public body
> {
> public:
> virtual result_type call();
> ...
> adaptee_type adaptee;
> };
>
> body * callee;
> };

What is coded above is

Cloned:
   _____ ____________________
        | | |
 handle |---->| adaptor 2 |
   _____| | __________________ |
              || adaptor 1 ||
              || ________________ ||
              ||| body |||
              ||| ______________ |||
              |||| user functor ||||
              |||| ||||
              |____________________|

Counted:
   _____ ____________________
        | | |
 handle |---->| adaptor 2 |
   _____| | __________________ |
              || adaptor 1 ||
              || ________________ ||
              ||| body |||
              ||| (counter) |||
              ||| ______________ |||
              |||| user functor ||||
              |||| ||||
              |____________________|

> The only difference between the two variants at this level is that
> cloning requires one more vtable entry than the reference counting
> version and the reference counting version, which uses a directly
> counted body, requires extra space per body instance for a counter. They
> are otherwise pretty much the same in terms of their impact on system
> resources, with roughly the same code overhead and roughly the same
> working set for a single instance. The difference becomes apparent with
> copying, but the point is that basic structure is not dissimilar.

Under both the counted and the cloned you do need a
body::body(const body&) and an adaptor<*>::adaptor(const adaptor<*>&),
inorder to form composites. Thus the only difference is on the copy.
Because of compositing you will require many variants. That is

For code like
  callback<void,int> c=bind(function_functor(&foo),A);
  callback<void,int> c2=bind(user_functor(),A);

You will get

  adaptor_bind<A,function_body<void,int,A> >
  adaptor_bind<A,body<user_functor> >
  body<user_functor>
  function_body<void,int,A>
  

The alternative is
  _______ _______ _______ _________________
         | | | | | | |
  handle |-->| adp 1 |-->| adp 2 |-->| generic_functor |
  _______| |(count)| |(count)| | (counter) |
             |_______| |_______| | _______________ |
                                     || user functor ||
                                     |_________________|

Which implements only
  adaptor_bind<void,int,A>
  body<void,int,A,user_functor>
  function_body<void,int,A>

And requires no copy constructors. The cost is
an extra heap size of 1 counters, 1 pointer, and 1 vtables.
But this is offset if slot is every copied or shared.

This is a runtime size verses speed verses compile size.
The minimum size (cloned) has the worst copy and largest
potential binary size.

 
> >> I'm not sure that I understand what you mean by operator= being
> >> implemented lots. Both the reference-counted and cloning designs must
> >> implement operator=, but in one case clone is called and in the other
> >> the count is incremented. However, roughly the same amount of code --
> >> which is small -- will be generated in each case for operator=.
> >
> >Okay let me explain this better.... assume we have 4 levels of indirection
> >2 adaptor one of which introduced some objects, the generic functor, and
> >then the user functor (a function ptr in this case).
> >
> >(in sigc++ terms, this is a rather extreme case)
> >
> > void foo(int,A);
> > Slot1<int,int> s1=retbind(bind(slot(&foo),A(10)),1);
> > Slot1<int,int> s2;
> > s2=s1; // <-- this statement
> >
> >In the case of cloning we have to have an
> > adp_rettype<int, adp_bind<A, func_functor<void,int,A> > >::copy
> > adp_bind<A, func_functor<void,int,A>::copy
> > A::copy
> > func_functor<void,int,A>::copy
> >
> >where copy is either operator= or X::X(const X&) depending on the
> >implementation. If I call this 1000 times in a row this would be
> >significant. Further, every different set of adaptors and
> >functors will generate a new set of assignment operator or copy
> >constructor. (Basically this is a type explosion.)
>
> Right, you are working to a different set of assumptions -- and indeed
> quite a different design -- which is why we have arrived at different
> answers. There is no difference in the number of assignment operators
> generated (only one) between reference-counted and cloning versions in
> the code I outlined above.

Correct.

I should also note that with the composite design you get the same
number of mallocs as the chained design on constructions.

(just assume we have 2 types of adaptors, bind and rettype)
  
   callback<int> c= rettype<int>(bind<int>(function_functor(&f),1),1);

Will need to alloc once for the function_functor then copy
to make the bind_adaptor composite and then alloc and copy to make the
rettype adaptor. It will then delete the other 2 copies.

Under the non-composite it allocs once for each adaptor but
it doesn't free any of them.

(not I am not trying to change you mind on designs, but instead
flush out the design choices we are making as I am implementing
sigc++ 1.1 right now with the design decisions I propose.)

--Karl


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