2011-04-30

Specifications, part III

Sometimes we write a code to be extensible, or to be tuned by code implemented by fellow developer. In C++ it is achieved most often by using virtual functions and templates. Using these features implies that some extra care should be taken: we must specify how exactly virtual function could be overridden, or what restrictions on type arguments should be met.

Implementing and overriding virtual functions

It is responsibility of class where virtual function was first declared ("most base" class) to specify how it should be implemented (if was pure), or how it could be overridden (if it wasn't). Consequently, it is implementor's/overrider's responsibility to obey these specifications. (For the sake of consiseness, I will use term "override" to describe both implementing and real overriding.)

For base class' programmer that means specifying carefully what pre-/postconditions and other special restrictions should implementation code meet and ensuring that client's code (one that uses base class) is correct when these requirements are indeed met.

For derived classes' programmer that means careful implementing the specification. Roughly speaking, you've implemented/overridden virtual member-function correctly, if it "requires no more, provides no less" than what was prescribed by base class. In particular that means:
  • preconditions on overriden version could not be stricter than base class specification;
  • correspondingly, postconditions should not be weaker than specification said they could be;
  • overridden version should obey exception guarantee (if any) promised by base class, and should not throw exceptions other than what is permitted by base class;
  • runtime/space complexity of overridden version should not exceed what specification claims.

Please note, that if base class has chosen to provide default implementation, it should itself play these rules. It sounds trivial, but way too often we see "error: not implemented" crap inside default version, or  default version being empty even if specification says that member-function should make some useful job instead.

Following these rules means that your code satisfies Liskov Substitution Principle. This principle is the single most important tool for ensuring correctness of "object-oriented code" -- i.e. the code that uses runtime polymorphism. If you have violated, it, you'd open the Pandora box of bugs, because there it is not possible for base class to satisfy its promises for clients without proper derived class support:

    struct Base
    {
        /// \pre param0 is greater than 42
        /// \returns some even number
        /// \throws runtime_error
        virtual int calculate_something(int param0) = 0;
    
        virtual ~Base() {}
    };
   
    struct BadGuy : Base
    {
        /// \pre param0 is greater than 100 <-- (1)
        /// \returns some number <-- (2)
        /// \throws bad_alloc for some weird reason <-- (3)
        int calculate_something(int param0);
    };
    
    struct GoodGuy : Base
    {
        /// \pre param0 is greater than 0
        /// \returns number that is multiple of 4
        /// \throws never
        int calculate_something(int param0);
    };

BadGuy is bad because it is mere "subcontractor" of Base, but it breaches its contract on every step:
  1. clients of Base know nothing about more strict requirement that BadGuy states on its argument;
  2. they expect even result, not just some result;
  3. probably they are not prepared to bad_alloc.
On the contrast GoodGuy::calculate_something() satisfies specification of Base's version. It even provides more than Base promises with weaken preconditions; though clients usually cannot directly take advantage from that -- as in good OO-code clients should rarely know about implementors of interfaces they are working with.

Simple and robust techniques to actually ensure that implementors behave well (at least that they met pre-/postconditions), is to use Template Method pattern, or even better its extreme version -- NVI. In this case you can put nice assert in non-virtual thunk, and at least test something.

    struct Base
    {
        /// \pre param0 is greater than 42
        /// \returns some even number
        /// \throws runtime_error
        int calculate_something(int param0)
        {
            assert(param0 > 42);
            do_calculate_something(param0);
        }
    private:
        /// \pre param0 is greater than 42

        /// \returns some even number

        /// \throws runtime_error

        virtual int do_calculate_something(int param0) = 0;
    };

Obviously it doesn't work for "advanced" stuff (complexity, exceptions, etc.), but it is still better to at least check what you can. (Though sometimes you cannot or don't want to use that techniques)

Next time I will write about templates and type requirements.