arlo on 17 May 2003 04:16:01 -0000


[Date Prev] [Date Next] [Thread Prev] [Thread Next] [Date Index] [Thread Index]

[ALACPP] Making solutions combinitorial


The discussion on Chris' evil code trick today reminded me of a discussion
that I had at the meeting last week abotu code re-use.

Basically, the problem with the first several solutions to Chris' problem
(including my function overloading one) is that we were looking at the
wrong place. The _function_ is not wrong, the _types_ of the argument and
return values are wrong. Thus, for a solution, we should look not toward
the function, but toward just those types, and let the compiler then do
its magic to make exactly the needed changes in the output, and no more.

The reason that this crosslinks with the earlier discussion is because it
also gives us a combinatorial solution. By focusing on the types, we end
up with a solution (type computers as traits, or whatever) that is
re-usable across any number of function declarations. Rather than having
to change each function that falls into the pattern (perhaps invasively,
as Jon suggested), we need only create one orthogonal tool, and then use
it wherever it is needed. Thus, our solution becomes combinitorial with
all of our past general solutions, and can be applied uniformly.

For example, what if we were using volatile to allow the compiler to check
for missing locks (see Alexandrescu's article for more info. I'm sorry,
but I don't have the citation here. Jon might)? Then, the usual approach
is to provide two implementations of every interface member function, eg:

class Foo
{
  // ...

  /* ... */ doSomething(/* ... */)
  {
    dataModifyOp1();
    dataReadOp1();
    dataModifyOp2();
    return dataReadOp2();
  }
  /* ... */ doSomething(/* ... */) volatile
  {
    LockingPointer< Foo >(this)->dataModifyOp1();
    dataReadOp1();
    LockingPointer< Foo >(this)->dataModifyOp2();
    return dataReadOp2();
  }
};

Now, users simply have all of their data members volatile or not,
depending on their manner of use. Net result is that the compiler uses the
locks when called from multithreaded (volatile) code, but not when called
from single-threaded (non-volatile, already locked) code, within the one
executable.

The problem is again duplication of function definition, and one solution
is similar to the solution that I gave to Chris' problem:

//  --- in LockingPtr.h

template< tyepname T >
class NullLockingPtr
{
  NullLockingPtr(T *val) : val_(val) {}
  T& operator*() const volatile { return val_; }
  T *val_;
};

template< typename T >
class LockingPtrT { typedef LockingPtr< T > type };

template< typename T >
class LockingPtrT< T volatile > { typedef NullLockingPtr< T > type };

template< typename T >
inline LockingPtrT::type MakeLockingPtr(T *ptr)
{
  return LockingPtrT::type(ptr);
}

#define DECLARE_BOTH(fn_sig, fn_def) \
  fn_sig fn_def \
  fn_sig volatile fn_def

//  --- in using code:

#include <LockingPtr.h>

class Foo
{
  // ...

  DECLARE_BOTH(
  /* ... */ doSomething(/* ... */),
  {
    MakeLockingPtrT(this)->dataModifyOp1();
    dataReadOp1();
    MakeLockingPtrT(this)->dataModifyOp2();
    return dataReadOp2();
  } /* the one on the left is a brace. On the right is a paren. */ )
};

, and now you don't have to re-declare the bodies of any of your
functions, but you still get locking at the sub-operation level (locking
only modifications to variables of builtin data types, and so avoiding
most deadlocks). The compiler figures out from the callsite which overload
to call, and the overloads are syntactically identical, using type
computers to determine the behavior based on the type of the this pointer.

Note that the dataReadOps may, in fact, call member functions on
full-class objects, and they don't take out locks first. However, those
objects also have a volatile and a non-volatile interface, and do all
locking to make things work. Likewise, if an object needs to critical
section a set of operations, it can do so within the function, although
this can lead to deadlocks.

...So....

Back to the original topic: how this relates to Chris' params passing
question.

What if the doSomething member function met Chris' pattern? Without either
of the solutions, you'd have to repeat the body 4 times, for the 4
combinations of const and volatile. However, the two solutions are
orthogonal, and so you can state it just once, and let the preprocessor
and the compiler figure it all out:

class Foo
{
  // ...

  DECLARE_BOTH(
  template< ArgT >
  call_traits< ArgT >::ref_return_type
  doSomething(call_traits< ArgT >::param_type param),
  {
    MakeLockingPtrT(this)->dataModifyOp1();
    dataReadOp1();
    MakeLockingPtrT(this)->dataModifyOp2();
    return dataReadOp2();
  } /* the one on the left is a brace. On the right is a paren. */ )
};

Phew!

The function signature now carries a lot of data with it, but all of that
data is boilerplate (and as simple as I can make it, so far - can you
simplify it more?). Thus, after you do the other 7 interface member
functions for this class, plus the other classes in your system, you'll
get used to it. There aren't any subtlties in it that change from use site
to use site - everything is extracted into type computers. Thus, you don't
have to think about it at every use site, only at the type computers. So,
the site is a little uglier, but the ugly can be safely ignored after the
first hour or two that you work with it.

The solution approach is identical in both cases, and is a good way to
achieve generic, reusable code: operate on abstract syntax trees. Use
templates to factor out all differences, so that you can use the same
syntax to refer to different results. Templates can not be used to create
multiple declarations (eg, member functions that differ only by the type
of their this pointer), so use the preprocessor to state only the multiple
declarations. Because everything shares identical syntax, it can now be
used from things that aren't aware that there's an indirection point
there. Multiple solutions work together because each gives the appearance
of being constant, not variable.

Arlo


_______________________________________________
alacpp mailing list
alacpp@xxxxxxxxxxx
http://lists.ellipsis.cx/mailman/listinfo/alacpp