Re: [boost] library for type erasure

AMDG Alexander,
Steven, do you have a document that describes your approach in more details? Does it support multimethods for binary operations?
-- Alexander Nasonov
No I don't have such a document so here are the main benefits and drawbacks of my approach. I didn't know about your library until yesterday so I only understand it in general but I will try to primarily note the differences: I am using a table of function pointers and an unchecked variant type(like Boost.Function) instead of using virtual functions. Thus I avoid a heap allocation for objects that are small enough and are correctly aligned. On the other hand this implementation requires the cast methods to only convert to the exact type put in - not to any of it's base classes. The class template erasure is declared as template<class Operations, class Policy = default_policy, class Derived = my_own_type> class erasure; Operations is a composite tree of operations to allow groups of operations to behave the same as single operations. So all three of these are equivalent typedef combine_operations<boost::mpl::vector<isreamable, ostreamable> > ops; erasure<ops> any; typedef combine_operations<istreamable> ops; erasure<boost::mpl::vector<ops, ostreamable> > any; erasure<boost::mpl::vector<isreamable, ostreamable> > any; Policy specifies the details of the object structure. I have implemented a policy that simulates a virtual function based approach but this seems to put excessive strain on the compiler because of the extra dependencies it introduces. Derived is to allow members like assignment which return(*this) to cast to the correct type. Because of the two other parameters I don't allow things like erasure<istreamable, ostreamable> like of Boost.Variant although I think I could add support for it(with some ugly metafunctions and more coupling). I have provided support for Boost.Ref int i; erasure<...> any(boost::ref(i)); int& iref = erasure_cast<int&>(any); assert(&i == &iref); const int* iptr = erasure_cast<const int*>(*any); assert(iptr == & iref); The price of allowing the addition of const(or volatile) is a very repetitive section of code and 1-2 extra integer comparisons. One final optimization I have implemented is for conversions from one type to a subset. I think that this will slow down the assignment(especially in a multi-threaded program) but will speed up subsequent operations on the result. erasure<istreamable, ostreamable> any_io(1); erasure<ostreamable> any_out(any_io); assert(erasure_cast<const int&>(any_out) == 1); I do not support multi-methods. Unfortunately, there is no way to implement a binary operation that takes two arbitrary types. The closest I can get is to have an operation that takes one arbitrary type and one type from some fixed set which may depend on the first type. I didn't even think of this possibility until you mentioned it so I havn't actually tried to implement it. Finally, here is an example of how I am defining operations: struct addition1 : operation<addition1> { template<class T, class Any> struct static_impl : implementation<void (T&, const Any&)> { static void execute(T& arg, const Any& other) { arg += erasure_cast<const T&>(other); //throws bad_erasure_cast } }; template<class Base> struct partial_interface : Base { typename Base::erasure_full_t& operator+=(const typename Base::erasure_full_t& other) { erasures::call<addition1>(*this, other); //throws object_is_empty return(*erasures::full(this)); } }; }; From the above you can see that the syntax for defining operations is somewhat less than ideal. The main problem I know of is that defining overloaded functions requires a workaround to prevent the derived class version from hiding the base class one. Thus the preceeding example becomes: struct addition1 : operation<addition1> { template<class T, class Any> struct static_impl : implementation<void (T&, const Any&)> { static void execute(T& arg, const Any& other) { arg += erasure_cast<const T&>(other); } }; template<class Base> struct partial_interface_impl : Base { typename Base::erasure_full_t& operator+=(const typename Base::erasure_full_t& other) { erasures::call<addition1>(*this, other); return(*erasures::full(this)); } }; template<class Base> struct partial_interface : partial_interface_impl<Base> { using partial_interface_impl<Base>::operator+=; }; }; If you have any furthur questions I would be glad to answer them. In Christ, Steven Watanabe

Steven Watanabe wrote:
[ skiped ] Finally, here is an example of how I am defining operations: [ skiped ]
I also use CRTP trick but I pass a signature of an operation as well: struct add_int_constant : function<add_int_constant, anyT (anyT const&, int)> { // ... }; Here anyT is a placeholder for any<OperationList>. It lets me distinguish between any<OperationList> and regular arguments. Base class has operator() that deduces OperationList from a first anyT argument. So a user can just call the add_int_constant functor: any< mpl::vector<add_int_constant> > a(9); a = add_int_constant()(a, 11); assert(extract<int>(a) == 20); There is no stable interface for operation implementation yet. It should be simple for very generic operation like to_ostream and for an opposite case of different functions for every combination of types. For the latter I'm thinking of the following approach: struct plus : function<plus, anyT (anyT const&, anyT const&)> { int entry(id<1>, int, int) const; double entry(id<2>, double, int) const; // ... std::string entry(id<100>, std::string const&, char) const; // ... }; Basically, it's multimethods where all entries belong to one class. Using id<N> agruments, it's possible to view entries as MPL immutable sequence of signatures (typeof is required). Obviously, such entries can't be templates. If OperationList is needed, a user could resort to a nested "definition" template (pay attention to entry 200): struct plus : function<plus, anyT (anyT const&, anyT const&)> { template<class L> // L is OperationList struct definition { int entry(id<1>, int, int) const; double entry(id<2>, double, int) const; // ... any<L> entry(id<200>, any<L> const&, int) const; }; }; I believe that SFINAE can detect a presence of "definition". Operations with only one anyT agrument can be implemented without using id<N> agruments: struct to_ostream : function<to_ostream, ostream& (ostream&, anyT const&)> { template<class T> ostream& entry(ostream& o , T const& t) const { return o << t; } ostream& entry(ostream& o, emptyT) const { return o; } }; Similarly, "definition" can be used if required. -- Alexander Nasonov Project Manager at Akmosoft ( http://www.akmosoft.com ) Blog: http://nasonov.blogspot.com Email: $(FirstName) dot $(LastName) at gmail dot com
participants (2)
-
Alexander Nasonov
-
Steven Watanabe