topical media & game development

talk show tell print

object-oriented programming

Polymorphism

subsections:


Polymorphism is an intriguing notion. Briefly put, polymorphism is the ability of a particular entity (which may be an object, a function, or a variable) to present itself as belonging to multiple types. Object-oriented languages are not unique in their support for polymorphism, but it is safe to say that polymorphism is an important feature of object-oriented languages. As explained in chapter 9, polymorphism comes in various flavors. With regard to object-oriented languages, we usually mean inheritance or inclusion polymorphism. Even within this restricted interpretation, we have to make a distinction between syntactic polymorphism, which requires merely that interfaces conform, and semantic polymorphism, where conformance requirements also include behavioral properties.

In this section, we will look at some simple examples in Java that illustrate how we may use the mechanisms of inheritance and (simple) delegation to define objects that have similar functionality but differ in the way that functionality is realized. These examples prepare the way for the more complex idioms and patterns presented later in this chapter.

In the rest of this section we will look briefly at the polymorphic constructs offered by C++. We will also study how behavioral conformance can be enforced in C++ by including invariants and assertions. These sections may be skipped on first reading.

Inheritance and delegation in Java

Consider the example below, an envelope class that offers a message method. In this form it is nothing but a variation on the hello world example presented in the appendix.


  public class envelope { 
envelope
public envelope() { } public void message() { System.out.println("hello ... "); } };

slide: Hello World

To illustrate the idea underlying idioms and patterns in its most simple form, we will refine the envelope class into the collection of classes depicted in slide 2-envelope-letter.

We will proceed in three steps: (1) The envelope class will be redesigned so that it acts only as an interface to the letter implementation class. (2) Then we introduce a factory object, that is used to create envelope and letter instances. (3) Finally, we refine the letter class into a singleton class, that prevents the creation of multiple letter instances.



slide: Envelope/Letter Factory

Envelope/Letter

The Envelope/Letter idiom was introduced in  [Coplien92] as a means to separate interface aspects from implementation aspects. Here the call to message is simply forwarded to the letter object.

  public class envelope { 
envelope
letter impl; public envelope() { impl = new letter(); } public void message() { impl.message(); } };


  public class letter { 
letter
public letter() { } public void message() { System.out.println("Message in a letter"); } };

slide: Envelope/Letter

Admittedly, there is no need here to make such a distinction, but the idea speaks for itself. As you will see, this distinction allows us to change the implementation without modifying the envelope or interface class.

Factory

In the next refinement, we introduce a factory object, that allows us to create envelope and letter instances without invoking a constructor.

  public class factory { 
factory
public factory() { } letter letter() { return new letter(); } envelope envelope() { return new envelope(); } };

  public class envelope { 
envelope
letter impl; public envelope() { factory f = new factory(); impl = f.letter(); // obtained from factory } public void message() { impl.message(); } };

slide: Factory

The factory object is used in the envelope class to create a letter. The advantage here, as will be shown shortly, is that the envelope class does not need to have any information about the actual type of the letter.

Singleton letter

Finally, we refine the letter class into a singleton class. When you inspect the implementation, you will see that only one instance of a letter will be created.

  public class singleton extends letter {  
singleton
static int number = 0; protected singleton() { } static letter instance() { if (number==0) { theletter = new letter(); number = 1; } return theletter; } public void message() { System.out.println("Message in a letter"); } static letter theletter; };

slide: Singleton letter

Note that the factory object must be modified so that the static method instance of singleton is invoked instead of the original constructor of letter.

Discussion

This example, however simple, demonstrates the implementation of some of the idioms and patterns that will be discussed in the rest of this chapter. It shows that the basic mechanisms of inheritance and simple delegation or forwarding are sufficient to implement these idioms and patterns. We have not discussed yet why we need idioms and patterns, but this will hopefully become clear later on.

Polymorphism in C++

Polymorphism essentially characterizes the type of a variable, function or object. Polymorphism may be due to overloading, parametrized types or inheritance. Polymorphism due to inheritance is often considered as the greatest contribution of object-oriented languages. This may be true, but the importance of generic (template) types and overloading should not be overlooked.

Overloading

print



  extern void print(int);
  extern void print(float);
  

Generic class -- templates

list< T >


  template< class T > class list { ... }
  list* alist; 
  

Polymorphism by inheritance

shape



  class shape { ... };
  class circle : public shape { ... } 
  shape* s = new circle;
  

slide: Polymorphic type declarations

In slide 1-polymorphism some examples are given of declarations involving polymorphic types. The function print is separately defined for int and float. Also, a generic list class is defined by means by employing templates. The list may be used for any kind of objects, for example integers. Finally, a shape class is defined from which a circle class is derived. An instance of the circle may be referred to by using a shape pointer, because the type shape encompasses circle objects.

The Standard Template Library (STL)

The Standard Template Library for C++ provides a generic library of data structures to store, access and manipulate data. It is a generic library based on templates. In fact, it uses templates in such an aggressive way that the C++ standardization committee was forced to reconsider its definition of the generic template facility in C++. See  [STL].

Standard Template Library

STL


  • containers -- to hold objects
  • algorithms -- act on containers
  • iterators -- to traverse containers
  • functions -- as objects
  • adaptors -- to transform objects
  • allocators -- for memory management

slide: The Standard Template Library

The Standard Template Library (STL) offers containers, to hold objects, algorithms, that act on containers, and iterators, to traverse containers. Algorithms, which are implemented as objects, may use functions, which are also defined as objects, overloading the application operator() method. In addition, STL offers adaptors, to transform objects, and allocators, for memory management.

STL is supported by C++ compilers that adhere to the C++ standard, including Microsoft Visual C++ and the Cygnus/GNU C++ compilers. A more extensive discussion of STL is beyond the scope of this book, but the reader is advised to consult  [STL], which gives an introduction to STL and its history, as well as a thorough course on programming with STL.

Assertions in C++

Whatever support a language may offer, reliable software is to a large extent the result of a disciplined approach to programming. The use of assertions has long since been recognized as a powerful way in which to check whether the functional behavior of a program corresponds with its intended behavior. In effect, many programming language environments support the use of assertions in some way. For example, both C and C++ define a macro assert which checks for the result of a boolean expression and stops the execution if the expression is false.

In the example below, assertions are used to check for the satisfying of both the pre- and post-conditions of a function that computes the square root of its argument, employing a method known as Newton iteration.



  double sqrt( double arg ) { 
sqrt
require ( arg >= 0 ); double r=arg, x=1, eps=0.0001; while( fabs(r - x) > eps ) { r=x; x=r-((r*r-arg)/(2*r)); } promise ( r - arg * arg <= eps ); return r; }

slide: Using assertions in C++

In the example, the macro assert has been renamed require and promise to indicate whether the assertion serves as, respectively, a pre- or post-condition. As the example above shows, assertions provide a powerful means by which to characterize the behavior of functions, especially in those cases where the algorithmic structure itself does not give a good clue as to what the function is meant to do. The use of assertions has been promoted in  [Meyer88] as a design method for object-oriented programming in Eiffel. The idea is to define the functionality of the various methods by means of pre- and post-conditions stating in a precise manner the requirements that clients of an object must meet and the obligations an object has when executing a method. Together, the collection of methods annotated with pre- and post-conditions may be regarded as a contract between the object and its potential clients. See section contracts.

Whereas Eiffel directly supports the use of assertions by allowing access to the value of an instance variable before the execution of a method through the keyword old, the C++ programmer must rely on explicit programming to be able to compare the state before an operation with the state after the operation.



  class counter { 
counter
public: counter(int n = 0) : _n(n) { require( n >= 0 ); promise( invariant() ); // check initial state } virtual void operator++() { require( true ); // empty pre-condition hold(); // save the previous state _n += 1; promise( _n == old_n + 1 && invariant() ); } int value() const { return _n; } // no side effects virtual bool invariant() { return value() >= 0; } protected: int _n; int old_n; virtual void hold() { old_n = n; } };

slide: The counter contract

The annotated counter above includes a member function hold to store the value of its instance variable. It is used in the operator++ function to check whether the new value of the counter is indeed the result of incrementing the old value.

Assertions may also be used to check whether the object is correctly initialized. The pre-condition stated in the constructor requires that the counter must start with a value not less than zero. In addition, the constructor checks whether the class invariant, stated in the (virtual) member function invariant, is satisfied. Similarly, after checking whether the post-condition of the operator++ function is true, the invariant is checked as well.



  class bounded : public counter { 
bounded
public: bounded(int b = MAXINT) : counter(0), max(b) {} void operator++() { require( value() < max ); // to prevent overflow counter::operator++(); } bool invariant() { return value() <= max && counter::invariant(); } private: int max; };

slide: Refining the counter contract

When employing inheritance, care must be taken that the invariance requirements of the base class are not violated. The class bounded, given above, refines the class counter by imposing an additional constraint that the value of the (bounded) counter must not exceed some user-defined maximum. This constraint is checked in the invariant function, together with the original counter::invariant(), which was declared virtual to allow for overriding by inheritance. In addition, the increment operator++ function contains an extra pre-condition to check whether the state of the (bounded) counter allows it to perform the operation.

From a formal perspective, the use of assertions may be regarded as a way of augmenting the type system supported by object-oriented languages. More importantly, from a software engineering perspective, the use of assertions is a means to enforce contractual obligations.

Canonical class idioms

The multitude of constructs available in C++ to support object-oriented programming may lead the reader to think that object-oriented programming is not at all meant to reduce the complexity of programming but rather to increase it, for the joy of programming so to speak. This impression is partly justified, since the number and complexity of constructs is at first sight indeed slightly bewildering. However, it is necessary to realize that each of the constructs introduced (classes, constructors and destructors, protection mechanisms, type conversion, overloading, virtual functions and dynamic binding) may in some way be essential to support object-oriented programming in a type-safe, and yet convenient, way.

Having studied the mechanisms, the next step is to find proper ways, recipes as it were, to use these mechanisms. What we need, in the terminology of  [Coplien92], are idioms, that is established ways of solving particular problems with the mechanisms we have available. In his excellent book, Coplien discusses a number of advanced C++ idioms for a variety of problem domains, including signal processing and symbolic computing.

In this section, we will look at the concrete class idiom for C++, which states the ingredients that every class must have to behave as if it were a built-in type. Other idioms, in particular an improved version of the handle/body or envelope/letter idiom that may be used to separate interface from implementation, will be treated in the next section.

Concrete data types in C++

A concrete data type is the realization of an abstract data type. When a concrete data type is correctly implemented it must satisfy the requirements imposed by the definition of the abstract data type it realizes. These requirements specify what operations are defined for that type, and also their effects. In principle, these requirements may be formally specified, but in practice just an informal description is usually given. Apart from the demands imposed by a more abstract view of the functionality of the type, a programmer usually also wishes to meet other requirements, such as speed, efficiency in terms of storage and error conditions, to prevent the removal of an item from an empty stack, for example. The latter requirements may be characterized as requirements imposed by implementation concerns, whereas the former generally result from design considerations.

Canonical class in C++

  • default constructor
  • copy constructor
  • destructor
  • assignment
  • operators

Abstract data types must be indistinguishable from built-in types


slide: Canonical class

To verify whether a concrete data type meets the requirements imposed by the specification of the abstract data type is quite straightforward, although not always easy. However, the task of verifying whether a concrete data type is optimally implemented is rather less well defined. To arrive at an optimal implementation may involve a lot of skill and ingenuity, and in general it is hard to decide whether the right choices have been made. Establishing trade-offs and making choices, for better or worse, is a matter of experience, and crucially depends upon the skill in handling the tools and mechanisms available.

When defining concrete data types, the list of requirements defining the canonical class idiom given in slide 2-canonical may be used as a check list to determine whether all the necessary features of a class have been defined. Ultimately, the programmer should strive to realize abstract data types in such a way that their behavior is in some sense indistinguishable from the behavior of the built-in data types. Since this may involve a lot of work, this need not be a primary aim in the first stages of a software development project. But for class libraries to work properly, it is simply essential.



(C) Æliens 04/09/2009

You may not copy or print any of this material without explicit permission of the author or the publisher. In case of other copyright issues, contact the author.