Contents|Index|Previous|Next

Type Abstraction using Signatures

In GNU C++, you can use the keyword signature to define a completely abstract class interface as a datatype. You can connect this abstraction with actual classes using signature pointers. If you want to use signatures, run the GNU compiler with the -fhandle-signatures command-line option. (With this option, the compiler reserves a second keyword, sigof, as well, for a future extension.)

Roughly, signatures are type abstractions or interfaces of classes. Some other languages have similar facilities. C++ signatures are related to ML’s signatures, Haskell’s type classes, definition modules in Modula-2, interface modules in Modula-3, abstract types in Emerald, type modules in Trellis/Owl, categories in Scratchpad II, and types in POOL-I. For a more detailed discussion of signatures, see Signatures: A Language Extension for Improving Type Abstraction and Subtype Polymorphism in C++ by Gerald Baumgartner and Vincent F. Russo (Tech report CSD–TR–95–051, Dept. of Computer Sciences, Purdue University, August 1995, a slightly improved version appeared in Software—Practice & Experience, 25(8), pp. 863–889, August 1995). You can get the tech report by anonymous FTP from ftp.cs.purdue.edu in ‘pub/gb/Signature-design.ps.gz’.

Syntactically, a signature declaration is a collection of member function declarations and nested type declarations. For example, the following signature declaration defines a new abstract type ‘S’ with member functions, ‘int foo()’ and ‘int bar(int)’.

    signature S
    {
       int foo ();
       int bar (int);
    };

Since signature types do not include implementation definitions, you cannot write an instance of a signature directly. Instead, you can define a pointer to any class that contains the required interfaces as a signature pointer. Such a class implements the signature type.

To use a class as an implementation of S, you must ensure that the class has public member functions ‘int foo()’ and ‘int bar(int)’. The class can have other member functions as well, public or not; as long as it offers what’s declared in the signature, it is suitable as an implementation of that signature type.

For example, suppose that ‘C’ is a class that meets the requirements of signature ‘S’ (C conforms to S). Then the following statement defines a signature pointer ‘p’ and initializes it to point to an object of type ‘C’.

    C obj;
    S * p = &obj;

The member function call, int i = p->foo ();, executes obj.foo ().

Abstract virtual classes provide somewhat similar facilities in standard C++. There are two main advantages to using signatures instead.

- Subtyping becomes independent from inheritance.
A class or signature type ‘T’ is a subtype of a signature type ‘S’ independent of any inheritance hierarchy as long as all the member functions declared in ‘S’ are also found in ‘T’. So you can define a subtype hierarchy that is completely independent from any inheritance (implementation) hierarchy, instead of being forced to use types that mirror the class inheritance hierarchy.

- Signatures allow you to work with existing class hierarchies as implementations of a signature type. If those class hierarchies are only available in compiled form, you’re out of luck with abstract virtual classes, since an abstract virtual class cannot be retrofitted on top of existing class hierarchies.
So you would be required to write interface classes as subtypes of the abstract virtual class.
There is one more detail about signatures. A signature declaration can contain member function definitions as well as member function declarations. A signature member function with a full definition is called a default implementation; classes need not contain that particular interface in order to conform.
For example, a class ‘C’ can conform to the following signature.

    signature T
    {
       int f (int);
       int f0 () { return f (0); };
    };

This happens whether C implements the member function, ‘int f0()’, or not. If you define C::f0, that definition takes precedence; otherwise, the default implementation, S::f0, applies.