Intersecting Conformance
[Principles]

    Identifying Syntactic and Semantic Conformance between Conceptually Related Types while Avoiding Coupling.

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.

Named Conformance

Two types are named conformant if they share the same name. The significance of this is that types related by inheritance share the same name, that of their common base class. Consider the following class hierarchy:

 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.

Structural Conformance

Two types are structurally conformant if instances of them can be placed in syntactically identical constructs and exhibit (appropriately) similar semantics. In other words, they can both compile in the same code and will operate in the same (or suitably similar) manner.

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.

Note:
There are mechanisms for explicit conformance, such as Shims, Concept Tagging and Member Type Tagging, but these are outside the scope of the current discussion. (See Extended STL for more details.)

Intersecting Conformance

As discussed above, structural conformance is the correspondence between syntax and semantics for unrelated types, for a given syntactic context (or set of contexts).

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:

Emulate Missing Functionality

In this approach functionality present in one interface that is missing in the other is emulated. For example, we could emulate the 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.

Provide Only Common Functionality

Given an impossibility of emulating missing functionality for a given facade, one might think that the only reasonable approach is to provide only what functionality is, or can be readily made to be, common to both. In the case of the 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.

Stipulate Intersecting Functionality but Embrace Variety

Rather than attempt either of these absolutist approaches, the principle of Intersecting Conformance dictates that facades for disparate technologies should employ structural conformance only to the degree of the intersection of (meaningfully) identical functionality, rather than employing significant additional functionality to achieve total structural conformance. In other words, where functionality is identical, or very similar, structural conformance is employed to ensure exact syntactic correspondence. Where the functionality is different, such that conformant semantics is not possible, or realisable in an efficient and robust manner, conformance should be explicitly eschewed.

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: // 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();
};

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(bool bRecursive); process_mutex(int pshared, bool bRecursive);
    ~process_mutex();
public: // Operations
    void lock();
    bool try_lock();
    void unlock();
public: // Accessors
    handle_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.


Generated on Thu Jun 10 08:58:21 2010 for STLSoft by  doxygen 1.5.6