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