Boost logo

Boost :

From: Karl Nelson (kenelson_at_[hidden])
Date: 2000-11-20 16:15:25


> > You got it. This is only possible with target cooperation.
> > For this to work the target must derive or contain an SigC::Object.
> > If it isn't derived the second call will segfault on access
> to 'this'.
> > It is just the property of slots that they can handle this reverse
> case.
>
> Then doesn't it eliminate Slots from being used with generalized
> functors, such as those created by the standard library or libraries
> such as Lambda?

Not at all. You simply need to build a slot type for a general
functor or lambda and it will plug into the system easily.

Ie.

  boost::callback<void> cb= Functor();
  Slot0<void> sl= functor_slot(cb);
  sl(); // calls cb() -> Functor::operator()()

Note, that because slots must be formed with a type the user
would have to indicate which operator() they want to use to form.

Ie.

  struct MyFunctor
    {
      float operator()(float,float);
    };
   
   Slot2<float,int,int> sl=functor_slot<float,int,int>(MyFunctor());
                                       ^^^^^^^^^^^^^^
                      this is required so that it knows to make a
                      Slot2<float,int,int> to satisfy the operator =

SigC 1.1 is also clever about the reverse dependencies. Because
they cost a lot of size in extra overheads, targets that can't go
away like a function pointer or functor skip the
generation of the dependencies. Thus saving the time and space
they would involve. Thus a functor bridge to slot would be
use the same basic concepts as bind adaptor and function slot.

[...]
> >
> > How is the cascade achieved? When developing SigC I
> > tried two approaches. One was to copy the old type into
> > the new and the other was to separate by some neutral abstraction
> > (without a vtable.)
>
> I believe it copies, but I could be way wrong here. You should look
> at the Lambda library or talk to it's creators.

Okay, that causes same old type explosion. It is fine for many
systems, but would explode into massive bload in something like a
widget library. Obviously the tradeoffs here depend on the
application.

 
[...]
> > Yes. Likely this is the distinction. The slots are
> > composable from function and object/member function pairs only.
> > You can build your own slot types and thus make functors
> > but that not commonly done. (The new version will allow more tricks
> > like this
> >
> > Slot0<void> sl;
> > {
> > Signal0<void> sig;
> > sig.connect(slot(&foo));
> > sig.connect(slot(f,&Bar::bar));
> > sl=sig;
> > }
> > sl(); // calls both foo() and f.bar()
> >
> > which further increases the power of the multicast.)
>
> Nice "trick", especially if the overhead is managed by the
> implementation.

Because the signals internals are reference counted they can be
passed and converted to a slot which will then be able emit the
signal at a later point. Thus everything is automated such that
the user does not need to manage any of the internal workings.
SigC-1.0 lacked a lot of these type of functionality.

 
[...]
> > Since the template versions of the Slot are not derived from
> > a base class with a virtual but simply provide one static
> > function, I have sacraficed some speed and runtime overhead
> > (1 function pointer) for a reasonably large ammount of
> > binrary size. The second thing this factorization did was
> > allow most of my functionality to move from a header to the
> > libary implementation. Thus I get one 32k sharable chunck of
> > code which can be linked in with a shared library, where
> > a standard template implementation would have had no
> > implementation section whatsoever. (I am not explaining
> > this particularly well.)
> >
> > Is that clear?
>
> Not just yet. I see some of the magic here but I'll have to think on
> it for a bit before I really comment.

Template factorization of this type is a bit of micromanagement.
But then if you need to make a few hundred Signal/Slot types
in your headers it is worth it. This is usually a big binary
size verse speed trade off.

You could imagine I make a list<> class in which I wanted to
place a sort. I could either make the sort in the header and
thus if my list needs many list<> types with sort I get
one sort routine for each type. As an alternative I could make
the list class have a structure containing a set of operations
like next, prev, swap, compare functionality, then I write
one sort routine using these operations on a neutral list.
The result would be a list which is slower (because of indirection)
but produced much smaller binaries because it only had to implement
a set of those smaller factored operation functions rather than
the larger sort routine for each type used in the list.

 
 
[...]

>
> I think we still have a disconnect. Let me rephase yet again, being
> careful to spell it out: I'm suggeting that Boost Threads, with
> their portable mutex classes, could be used in the implementation of
> SigC to insure SigC's thread safety.

Well, anything could be used to implement SigC threading needs.
All it needs is a mutex and a keyed private. :-) SigC-1.1
includes a cheezy pthread wrapper, but even that is overkill.

  

> > Right, SigC requires target to be from Object to achieve this.
> > All SigC slots reside in the heap to meet the lifetime requirements.
>
> This is a tradeoff issue. The other implementations posted for
> callbacks take a different approach. The functor is copied once and
> then managed through ref-counting. The only issue is
> with "closures", where the object portion of the closure may be
> destroyed leaving the closure in an "invalid" state. I see the
> danger here, and knowing what SigC was supposed to address I can see
> the frequency with which this could occur.

In the GUI level library which SigC caters to, this is a very
frequent event. For your functors, this can happen pretty much
one way....

struct MyFunctor
  {
     A* a; // this is a remote object which exists outside functor
     MyFunctor(A* a_) : a(a_) {}
     void operator(int i) { a->some_function(i); }
  };

callback<void,int> cb;
  {
    A a;
    cb = MyFunctor(&a);
  }
cout << cb.empty() << endl; // it isn't empty.
cb(); // crash

If is possible to make this track, but without cooperation from
the target it will not prevent calling, and without cooperation
from the source (callback) the cb will sit connected. (If
this was then a multicallback we will grow in memory though
it is not technically a leak.)

> What I'm wondering,
> though, is if it might not be appropriate to require the programmer
> to manage such issues? I realize how difficult this could be, which
> is why you didn't leave it this way I'm sure, but at the same time it
> seems less intrusive. It's the intrusive nature of how you handle
> this that prevents generalized functors from being used. Considering
> the power that generalized functors provide, is the intrusive nature
> worth it? I ask, because this is an issue for the current Callback
> postings, as well as for any "closure" implementation, so the
> concepts go beyond the applicability to SigC.

Well, it really depends what this is being used for. If you
are just trying to add a small amount of extra functionalty to
a class (basically giving it a function defined at run time)
then this tracking is a waste. This is if you just want to
pluggin how to handle the start of a thread into a class.

If however, the callback can trigger a series of events in other
objects in the program which change ratically over the life of the
program, the SigC style tracking becomes a requirement. Event
queue, multicast, and GUI programs tend to stress these aspects.
If every time a GUI programmer wanted to shut down a window
they had to go back and find all the places which they passed
a closure using that object (and all of the copies those
using objects made in adaptors) then the user would likely
tear their hair out. Given that SigC requiring one
thing in the base class is really minor.

--Karl


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