This page discusses the well-known principles of Named Conformance and Structural Conformance, and a refinement of Structural Conformance appropriate to the design of lightweight facades: Intersecting Conformance.
class A { public: virtual char const *getName() const { return "A"; } }; class B : public A { public: virtual char const *getName() const { return "B"; } virtual size_t getId() const { return 1; } }; class C : public A { public: virtual char const *getName() const { return "C"; } size_t getId() const { return 2; } }; class D : public B { public: virtual char const *getName() const { return "D"; } virtual size_t getId() const { return 3; } };
Now assume we want to write a function that will print out the class name for instances of these classes:
void printClassName(A const &ra) { ra.getName(); } A a; B b; C c; D d; printClassName(a); // Prints "A" printClassName(b); // Prints "B" printClassName(c); // Prints "C" printClassName(d); // Prints "D"
This is nothing surprising (or at least it shouldn't be to any C++ programmer). It works because classes B
, C
, and D
are all (publicly) derived from A
, and because A::getName()
is declared virtual. Thus, invocation of getName()
on a given object reference (or pointer) depends not on the type of the reference (or pointer) but on the actual type of the object being referenced. This is known as Virtual Function Polymorphism, or Runtime Polymorphism or as just Polymorphism. (The latter, unqualified, name is no longer popular, for reasons that will become clear later in this document. Runtime Polymorphism is the recommended term, since it precisely describes what is going on: that the type is determined at runtime.)
We can say: Classes B
, C
and D
exhibit named conformance with A
.
So far so good.
Now assume that we want to write a function that will elicit the Id of instance of class B
(or its derived classes):
void printClassId(B const &rb) { std::cout << rb.getId() << std::endl; } B b; D d; printClassId(b); // Prints "1" printClassId(d); // Prints "3"
Again, we rely on vtable polymorphism, and that: Class D
exhibits named conformance with B
.
Obviously applying printClassId()
to an instance of A
will be a compile error, since A does not have a member getId()
:
A a; printClassId(a); // compile error: Cannot convert 'A' to 'B const &'
What should also be understood is that we equally cannot apply printClassId()
to an instance of C
. Even though C
has a getId()
method, it is not derived from B
, and thus is not compatible: it has a different name.
Hence: Class C
, is not named conformant with B
.
Put another way, the definition of printClassId()
imposes the constraint on any type to which we wish to apply it that it must be named-conformant with B
.
To display the Id from C we'd have to overload the printClassId()
function:
void printClassId(C const &rc) { std::cout << rc.getId() << std::endl; } C c; printClassId(c); // Prints "2"
Note the code duplication between the two overloads of printClassId()
. They're identical in syntax (and semantics) and yet are quite distinct functions.
Before we start, it's sensible to concede that, absent pre-processor acrobatics, structural conformance is primarily relevant when using templates.
Let's recast printClassId()
as a function template:
template <class T> void printClassId(T const &rt) { std::cout << rt.getId() << std::endl; }
The template parameter T
is used in the function signature, as (part of) the type of the object parameter.
No part of the function template definition mentions the actual type of the parameter. What is mentioned, however, is that a (a non-mutating reference to) an instance of T
has a member getId
which can be involved in a function call expression and, further, that the result of that expression can be used with std::cout
and std::endl
via the insertion operator.
(Note: what may seem relatively obvious syntax above can actually be quite involved and surprising. For example, getId()
could be a static method, or could be a member variable of a type that has a function call operator defined. Sometimes things can be even more exotic, involving enumerations and properties. The issue is discussed in greater depth in the chapter "Duck and Goose" from the forthcoming book Extended STL.)
This version of printClassId()
is compatible with instances of B
and D
, and also with C
.
B b; C c; D d; printClassId(b); // Prints "1" printClassId(c); // Prints "2" printClassId(d); // Prints "3"
Thus we can state: Classes B
, C
and D
are structurally conformant with respect to printClassId()
.
Note the difference in emphasis in this statement. First, we describe this in terms of a mutual conformance between the conformant types, as opposed to a relative conformance in the case of named conformance. Second, we define their structural conformance in terms of the requirements of a constraining component, in this case the function template printClassId()
. Further, it is important to understand that the enforced constraints of the constraining component are entirely syntactic. Naturally, there is an expectation that syntatically conformant types will also have the semantic conformance, but this is not something that the C++ language can enforce for us. Rather, we need to observe higher level conventions, such as member/method naming conventions.
When designing facades one continually meets the challenge of abstracting common functionality between disparate technologies/APIs. Operating systems APIs are a very good example of this.
Consider that we want to provide a facade over the Windows mutex kernel object. Like most Windows kernel objects, the mutex may, optionally, be named to facilitate sharing between processes, and/or carry security attributes. They may be created already in the 'acquired' state. They are always recursive (which is to say that a thread that owns a mutex may 'acquire' it again).
The WinSTL project defines the facade winstl::process_mutex, which has the following class interface (explicit
constructor qualifiers are not shown for clarity):
class winstl::process_mutex { public: // Member Types typedef HANDLE handle_type; typedef process_mutex class_type; public: // Construction process_mutex(); process_mutex(char const *mutexName); process_mutex(wchar_t const *mutexName); process_mutex(char const *mutexName, bool bInitiallyAcquired); process_mutex(wchar_t const *mutexName, bool bInitiallyAcquired); process_mutex(char const *mutexName, bool bInitiallyAcquired, SECURITY_ATTRIBUTES *securityAttrs); process_mutex(wchar_t const *mutexName, bool bInitiallyAcquired, SECURITY_ATTRIBUTES *securityAttrs); ~process_mutex(); public: // Operations void lock(); bool lock(DWORD waitMilliseconds); bool try_lock(); void unlock(); public: // Accessors handle_type get(); };
Now we want to provide a corresponding facade for the PThreads mutex (pthread_mutex_t
). Unlike the Windows kernel object, PThreads' mutexes may not be named and do not support (Windows-like) security information. They do allow sharing, but in a much different (and largely) incompatible way to Windows: the mutex must be created as shareable, and is shared between processes via shared memory, rather than by accessing a kernel handle as in Windows.
The UNIXSTL project defines the facade unixstl::process_mutex
, which has the following class interface (explicit
constructor qualifiers are not shown for clarity):
class unixstl::process_mutex { public: // Member Types typedef pthread_mutex_t *handle_type; typedef process_mutex class_type; public: // Construction process_mutex(); process_mutex(bool bRecursive); process_mutex(int pshared, bool bRecursive); ~process_mutex(); public: // Operations void lock(); bool try_lock(); void unlock(); public: // Accessors handle_type get(); };
Clearly, each class interface contains elements that the other misses. There are two conventional approaches to this situation:
winstl::process_mutex::wait(DWORD)
method for unixstl::process_mutex
by using a retry loop and a micro_sleep()
operation.However, such approaches can be ugly, or unsafe, or inefficient, and are often all three. Furthermore, there are many cases where no meaningful analogue functionality can be specified. For example, we cannot incline the Windows mutex kernel object to reject recursive acquisition to be in line with the PThreads version. Nor can we apply the equivalent of Windows security information to a PThreads mutex. These things are, to all intents and purposes, impossible.
process_mutex
, this would yield class interfaces as follows:
class winstl::process_mutex { public: // Member Types typedef HANDLE handle_type; typedef process_mutex class_type; public: // Construction process_mutex(); ~process_mutex(); public: // Operations void lock(); bool try_lock(); void unlock(); public: // Accessors handle_type get(); };
and:
class unixstl::process_mutex { public: // Member Types typedef pthread_mutex_t *handle_type; typedef process_mutex class_type; public: // Construction process_mutex(); ~process_mutex(); public: // Operations void lock(); bool try_lock(); void unlock(); public: // Accessors handle_type get(); };
And, since they're now virtually identical, there'd be little point in maintaining separate class definitions, and we would instead define a single class platformstl::process_mutex
:
class platformstl::process_mutex { public: // Member Types #if defined(PLATFORMSTL_OS_IS_UNIX) typedef pthread_mutex_t *handle_type; #elif defined(PLATFORMSTL_OS_IS_WIN32) typedef HANDLE handle_type; #else /* ? OS */ # error Operating system not discriminated #endif /* OS */ typedef process_mutex class_type; public: // Construction process_mutex(); ~process_mutex(); public: // Operations void lock(); bool try_lock(); void unlock(); public: // Accessors handle_type get(); };
This is useful, to be sure, but Windows users may find themselves needing to be able to use a named mutex, or apply security information. Worse, the issue of choice regarding recursive nature in UNIX is taken away from the user. If the platformstl::process_mutex now mandates recursion the UNIX user may pay unncessary performance costs; if the class mandates non-recursive nature then the class has significantly different behaviour depending on operating system, something that is very wrong and guaranteed to result in platformstl::process_mutex
being avoided like the plague.
Thus, the intersection of the functionality of winstl::process_mutex and unixstl::process_mutex, as shown in the definition of process_mutex given in the previous section, will be as follows:
class ????::process_mutex { public: // Member Types typedef ???? handle_type; typedef process_mutex class_type; public: // Construction process_mutex(); ~process_mutex(); public: // Operations void lock(); bool try_lock(); void unlock(); public: // Accessors handle_type get(); };
Consequently unixstl::process_mutex and winstl::process_mutex provide a meaningful subset of common functionality (highlighted below) but each retain their important particular features:
class winstl::process_mutex { public: // Member Types typedef HANDLE handle_type;typedef process_mutex class_type;public: // Constructionprocess_mutex();process_mutex(char const *mutexName); process_mutex(wchar_t const *mutexName); process_mutex(char const *mutexName, bool bInitiallyAcquired); process_mutex(wchar_t const *mutexName, bool bInitiallyAcquired); process_mutex(char const *mutexName, bool bInitiallyAcquired, SECURITY_ATTRIBUTES *securityAttrs); process_mutex(wchar_t const *mutexName, bool bInitiallyAcquired, SECURITY_ATTRIBUTES *securityAttrs);~process_mutex();public: // Operationsvoid lock();bool lock(DWORD waitMilliseconds);bool try_lock(); void unlock();public: // Accessorshandle_type get();};
and:
class unixstl::process_mutex { public: // Member Types typedef pthread_mutex_t *handle_type;typedef process_mutex class_type;public: // Constructionprocess_mutex();process_mutex(bool bRecursive); process_mutex(int pshared, bool bRecursive);~process_mutex();public: // Operationsvoid lock(); bool try_lock(); void unlock();public: // Accessorshandle_type get();};
In this way, rich but thin facades may be provided for a given operating system/technology area, while still supporting the construction of higher level components that rely on a common abstracted view of the services provided.