|
Re: Bolt-Ins for Contract and Extension Classes
|
Posted: Apr 27, 2005 7:28 PM
|
|
> <h2>Bolt-Ins</h2>
[snip]
I'll address all that at a later stage
> <h2>Separation of Concerns</h2> > > The software engineering notion of "separation of > concerns" refers to the fact that within a > well-engineered > piece of software, we should find code which is > responsible for a specific concern located within a single > module. > <p> > Following the principle of separation of concerns has the > benefit of reduction of code coupling, and improvement of > code cohesion. > These are both well-known good software engineering > practices. The advantages are that following these > principles more naturally leads > to more maintainable and reusable code. > <p> > Bolt-ins can be used to enforce separation of concerns, > and to reduce code coupling.
Nice point.
> <h2>Programming with Contracts</h2> > > When it comes to the concern of verification of > contractual conditions (i.e. preconditions, postconditions > and invariants), this is > commonly tangled with the implementation of the class > itself. For instance consider the following class: > > <pre> > class SimpleClass { > public: > Init() { > // do initialization > } > void DoSomething() { > // precondition: object must be initialized > // do something > } > } > </pre> > > There is here an implied precondition to calling > <tt>DoSomething()</tt> which is that the member function > <tt>Init()</tt> must have been previously > called on a particular instance. A common and somewhat > naive approach to assuring the precondition is done by > introducing a member variable: > > <pre> > class SimpleClass { > public: > SimpleClass() : mbInit(false) { > } > Init() { > // do initialization > mbInit = true; > } > void DoSomething() { > assert(mbInit); > // do something > } > private: > bool mbInit; > } > </pre> > > Notice that <tt>mbInit</tt> is solely used for verifying > that the class is used correctly. The problem is that it > probably will only be > needed during debug builds.
I dislike "debug vs release" language when talking about contract programming, and suggest it unwise of you to use it when writing about it. May I suggest that we talk about differentiating between builds with different aspects of contract programming set at different levels of activity? Naturally the most coarse grained and widely used is simply between debug (all on) and release (all off) but, IMO at least, real world systems lie somewhere in between (or they should do, anyway! <g>).
> The naive solution then would > be: > > <pre> > class SimpleClass { > public: > SimpleClass() > #ifdef _DEBUG > : mbInit(false) > #endif > { } > Init() { > #ifdef _DEBUG > // do initialization > mbInit = true; > #endif > } > void DoSomething() { > assert(mbInit); > // do something > } > private: > #ifdef _DEBUG > bool mbInit; > #endif > } > </pre>
Nit: use of _DEBUG is not advised, as it's not portable. Just as common an equivalent - on another platform ;) - is the absence of NDEBUG. It tend to discriminate an ACMELIB_DEBUG symbol taking into account compiler, platform and presence/absence of _DEBUG/NDEBUG, e.g.
> > Needlessly to say that is a mess. The problem results from > a failure to separate concerns modularly. One improved > solution is to > use inheritance: > > <pre> > class SimpleClass_contract : SimpleClass_impl { > public: > void SimpleClass() : mbInit(false) { > } > void Init() { > SimpleClass_impl::Init(); > mbInit = true; > } > void DoSomething() { > assert(mbInit); > SimpleClass_impl::DoSomething(); > } > private: > bool mbInit; > }; > > class SimpleClass_impl { > public: > void Init() { > // do initialization > } > void DoSomething() { > // do something > } > }; > > #ifdef _DEBUG > typedef SimpleClass_contract SimpleClass; > #else > typedef SimpleClass_impl SimpleClass; > #endif > </pre> > > This solution is good, but the contract verification class > is not reusable because it is coupled with that particular > implementation. > This arises because we have only partially separated the > concern of contract verification. > Ideally I want something reusable, so I will use a class > which inherits from its template parameters (a bolt-in). > > <pre> > template<typename Impl_T> > class Simple_contract : Impl_T { > public: > void Simple_contract() : mbInit(false) { > } > void Init() { > Impl_T::Init(); > mbInit = true; > } > void DoSomething() { > assert(mbInit); > Impl_T::DoSomething(); > } > private: > bool mbInit; > }; > > class SimpleClass_impl { > public: > void Init() { > // do initialization > } > void DoSomething() { > // do something > } > }; > > #ifdef _DEBUG > struct SimpleClass : > Simple_contract<SimpleClass> > { }; > #else > struct SimpleClass : > SimpleClass_impl > { }; > #endif > </pre>
There's a mistake here, isn't there? Shouldn't it be
#ifdef _DEBUG struct SimpleClass : Simple_contract<SimpleClass_impl> { }; #else
- which isn't a bolt-in, then - or am I misunderstanding?
> This new contract verification class can now be used with > other classes with similar interfaces: > > <pre> > class AnotherSimpleClass_impl { > public: > void Init() { > // do initialization > } > void DoSomething() { > // do something > } > }; > > #ifdef _DEBUG > struct AnotherSimpleClass : > Initializing_contract<AnotherSimpleClass> > { }; > #else > typedef AnotherSimpleClass_impl AnotherSimpleClass; > #endif > </pre>
Have I missed a step? Isn't Initializing_contract actually Simple_contract ?
> This demonstrates how through the diligent practice of > separation of concerns we can arrive at code which is more > reusable. > > <h2>Extension Classes</h2> > > The public member functions of many classes can be divided > up into two relatively distinct groups, core functions > and derived functions. The derived functions can be > thought of as syntactic sugar, that is functions which can > be > defined in terms of the core functions. These derived > functions are a good candidate to use within a bolt-in.
Although I tend to favour the non-member approach, this last sentence makes a sound point, and is agreeable (to me anyway <g>).
> Consider > the case of the following naive and minimalist string > implementation: > > <pre> > class MyString_impl { > public: > void SetCount(int n) { > m.resize(n); > } > void SetChar(int n, char c) { > m[n] = c; > } > char GetChar(int n) const { > return m[n]; > } > int Count() { > return m.size(); > } > private: > std::vector<char> m; > }; > </pre> > > This class is too minimalist to be of much use as-is, > however with this basic implementation you can implement > virtually every other > concievable string member function. By writing the derived > functions within a bolt-in, I can reuse the derived set of > memember functions > on any class which matches the code interface. For > instance consider the following bolt-in: > > <pre> > template<typename Impl_T> > class String_ext : public Impl_T { > public: > void Concat(const Impl_T& x) { > int n = Count(); > SetCount(n + x.Count()); > for (int i=n; < Count(); i++) { > SetChar(i, x.GetChar(i-n)); > } > } > void Assign(const Impl_T& x) { > SetCount(x.Count()); > for (int i=0; i < Count(); i++) { > SetChar(i, x.GetChar()); > } > } > Impl_T SubString(int n, int cnt) const { > Impl_T s; > s.SetCount(cnt); > for (int i=0; i < cnt; i++) { > s.SetChar(i, GetChar(i + n)); > } > return s; > } > // and so on and so forth > }; > </pre> > > This extension class can be bolted on to any compatible > class implementation as simply as: > > <pre> > typedef String_ext<MyString_impl> MyString; > </pre>
Yes! This is exactly Bolt-in-ness to a T. :-)
> The advantage of doing this is that the extension class > can be reused on any class matching the > core interface. This also reduces the coupling between > extension functions and the implementation > to a set of four functions. Refactoring and debugging the > code as a result becomes much easier. > <p> > We can also define a contract verification class for > MyString as follows: > > <pre> > template<typename Impl_T> > class String_contract : public Impl_T { > public: > void SetChar(int n, char c) { > assert(n < Count()); > Impl_T::SetChar(n); > assert(GetChar(n) == c); > } > char GetChar(int n) const { > assert(n < Count()); > return Impl_T::GetChar(n); > } > } > </pre> > > Tying everything together gives us: > > <pre> > #ifdef _DEBUG > typedef > edef String_ext<String_contract<MyString_impl> > > MyString; > #else > typedef String_ext<MyString_impl> MyString; > #endif > </pre> > > <h2>The Big Gotcha</h2> > > Hopefully I have demonstrated the theoretical advantages > of separating classes into reusable components as > bolt-ins. > There is however a big problem with all of this, > constructor inheritance. In C++ constructors are not > inherited, which really > puts a wrench in the gears (and makes me wish I was > working with Heron instead of C++). > <p> > One solution I am aware of is to use template > constructors. This is not a perfect solution though. > <p> > My currently preferred solution to this design conundrum > is to forego initializing construction entirely within my > libraries. > A rather drastic and contentious measure, but perhaps one > which may pay off, because it opens the door more widely > to sophisticated usage > of template parameter inheritance techniques such as > bolt-ins, contract verificiation classes, extensions > classes, and more. > Some debate on the merits of this approach can be found in > the comments of my earlier blog entry > <a > href="http://www.artima.com/weblogs/viewpost.jsp?thread=106 > 524">Two Stage Construction in C++ versus Initializing > Constructors</a>
Ok, now I get where you're coming from.
I have a very prosaic response: all this effort in making a set of related classes as templates, rather than as plain classes, is only worth it if one can apply the bolt-in class templates to other components. Given the strong structural requirement constraints on the bolt-ee from the bolt-in - i.e. String_contract's bolt-ee needs to provide SetChar() and GetChar() - then one is writing a specific bolt-in in any case, so the exta effort in writing appropriate specific forwarding constructors is not prohibitive.
Since semantic checking is done when template are instantiated, it is perfectly safe to define a 'better' Bolt-in than is required for all the bolt-ee types, because only those ones used will be checked for validity by the compiler. Yes, there's an issue of maintenance in making your bolt-in class keep up with the bolt-ee classes, but that's not different than doing a more conventional approach. Indeed, it's still likely to be a big saving, since you only need to change the bolt-in once to reflect a new facility irrespective of how many bolt-ee types evolve to express that facility.
An example of this controlled complexity is shown in STLSoft's sequence_container_veneer. Here are all its forwarding constructors, enabling it to support the construction of types bolt-ed from all STL-sequence containers:
template< typename T , typename F > class sequence_container_veneer : public T { . . .
// Forwarding constructors
/// Constructs from a range template <typename I> sequence_container_veneer(I i1, I i2) : parent_class_type(i1, i2) {} /// Constructs from a range with the given allocator template <typename I> sequence_container_veneer(I i1, I i2, allocator_type const &a) : parent_class_type(i1, i2, a) {} /// Constructs with the given number of elements (initialised with the given value) template <typename V> sequence_container_veneer(size_type n, V v) : parent_class_type(n, v) {} /// Constructs with the given number of elements (initialised with the given value) with the given allocator template <typename V> sequence_container_veneer(size_type n, V v, allocator_type const &a) : parent_class_type(n, v, a) {}
By contrast, the Synesis RefCounter bolt-in, which is a true generic bolt-in in all its scary ramifications, looks like the following:
template< typename T , typename _S_ , typename _C_ > class RefCounter : public T , public _S_ { public: typedef T ParentClass; typedef _S_ SyncClass; typedef _C_ CountClass; typedef RefCounter<T, _S_, _C_> Class; typedef _mP(ParentClass) ParentClassPtr; typedef _mP(Class) ClassPtr; typedef _mPC(Class) ClassConstPtr; typedef _mR(Class) ClassRef; typedef _mRC(Class) ClassConstRef;
// Construction public: RefCounter(); RefCounter(const Class &rhs); virtual ~RefCounter() SyThrow_None();
#ifdef __SYNSOFT_DBS_CONSTRUCTOR_TEMPLATE_SUPPORT // For compilers that support member templates, the following constructors // are provided. Note that the first variant (one arg plus initial ref // count) has to have a non-default ref-count argument so as to disambiguate // it from the standard constructor above, and each variant following has the // same constraint. Whilst less than optimal, this approach allows access to // the ctors of the wrapped class. template <typename N1> RefCounter(N1 &n1) : ParentClass(n1) , m_cRefs(CountClass::Count) {} #if defined(__DMC__) template <typename N1> RefCounter(N1 *n1) : ParentClass(n1) , m_cRefs(CountClass::Count) {} #endif /* compiler */
template <typename N1, typename N2> RefCounter(N1 &n1, N2 &n2) : ParentClass(n1, n2) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2> RefCounter(N1 &n1, N2 *n2) : ParentClass(n1, n2) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2> RefCounter(N1 *n1, N2 &n2) : ParentClass(n1, n2) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2> RefCounter(N1 *n1, N2 *n2) : ParentClass(n1, n2) , m_cRefs(CountClass::Count) {}
template <typename N1, typename N2, typename N3> RefCounter(N1 &n1, N2 &n2, N3 &n3) : ParentClass(n1, n2, n3) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2, typename N3> RefCounter(N1 &n1, N2 &n2, N3 *n3) : ParentClass(n1, n2, n3) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2, typename N3> RefCounter(N1 &n1, N2 *n2, N3 &n3) : ParentClass(n1, n2, n3) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2, typename N3> RefCounter(N1 &n1, N2 *n2, N3 *n3) : ParentClass(n1, n2, n3) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2, typename N3> RefCounter(N1 *n1, N2 &n2, N3 &n3) : ParentClass(n1, n2, n3) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2, typename N3> RefCounter(N1 *n1, N2 &n2, N3 *n3) : ParentClass(n1, n2, n3) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2, typename N3> RefCounter(N1 *n1, N2 *n2, N3 &n3) : ParentClass(n1, n2, n3) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2, typename N3> RefCounter(N1 *n1, N2 *n2, N3 *n3) : ParentClass(n1, n2, n3) , m_cRefs(CountClass::Count) {}
template <typename N1, typename N2, typename N3, typename N4> RefCounter(N1 &n1, N2 &n2, N3 &n3, N4 &n4) : ParentClass(n1, n2, n3, n4) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2, typename N3, typename N4> RefCounter(N1 &n1, N2 &n2, N3 &n3, N4 *n4) : ParentClass(n1, n2, n3, n4) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2, typename N3, typename N4> RefCounter(N1 &n1, N2 &n2, N3 *n3, N4 &n4) : ParentClass(n1, n2, n3, n4) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2, typename N3, typename N4> RefCounter(N1 &n1, N2 &n2, N3 *n3, N4 *n4) : ParentClass(n1, n2, n3, n4) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2, typename N3, typename N4> RefCounter(N1 &n1, N2 *n2, N3 &n3, N4 &n4) : ParentClass(n1, n2, n3, n4) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2, typename N3, typename N4> RefCounter(N1 &n1, N2 *n2, N3 &n3, N4 *n4) : ParentClass(n1, n2, n3, n4) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2, typename N3, typename N4> RefCounter(N1 &n1, N2 *n2, N3 *n3, N4 &n4) : ParentClass(n1, n2, n3, n4) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2, typename N3, typename N4> RefCounter(N1 &n1, N2 *n2, N3 *n3, N4 *n4) : ParentClass(n1, n2, n3, n4) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2, typename N3, typename N4> RefCounter(N1 *n1, N2 &n2, N3 &n3, N4 &n4) : ParentClass(n1, n2, n3, n4) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2, typename N3, typename N4> RefCounter(N1 *n1, N2 &n2, N3 &n3, N4 *n4) : ParentClass(n1, n2, n3, n4) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2, typename N3, typename N4> RefCounter(N1 *n1, N2 &n2, N3 *n3, N4 &n4) : ParentClass(n1, n2, n3, n4) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2, typename N3, typename N4> RefCounter(N1 *n1, N2 &n2, N3 *n3, N4 *n4) : ParentClass(n1, n2, n3, n4) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2, typename N3, typename N4> RefCounter(N1 *n1, N2 *n2, N3 &n3, N4 &n4) : ParentClass(n1, n2, n3, n4) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2, typename N3, typename N4> RefCounter(N1 *n1, N2 *n2, N3 &n3, N4 *n4) : ParentClass(n1, n2, n3, n4) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2, typename N3, typename N4> RefCounter(N1 *n1, N2 *n2, N3 *n3, N4 &n4) : ParentClass(n1, n2, n3, n4) , m_cRefs(CountClass::Count) {} template <typename N1, typename N2, typename N3, typename N4> RefCounter(N1 *n1, N2 *n2, N3 *n3, N4 *n4) : ParentClass(n1, n2, n3, n4) , m_cRefs(CountClass::Count) {}
. . . etc. etc. up to permutations for 8 params!!!!!!
};
I think this ably demonstrates the usefulness and elegance of tailored Bolt-ins, and the horror of fully generic ones.
;)
|
|