Search code examples
c++inheritancedoxygendesign-by-contractpreconditions

How to document/assert when inheritance induces a precondition for some final types only


Consider this simple base class Foo having a function foo calling a pure virtual function foo_, documented with Doxygen :

class Foo
{
  public:
    /** \brief This function logs x and does the job */
    void foo(double x);
  protected:
    /** \brief This function does the job */
    virtual void foo_(double x) = 0;
};

void Foo::foo(double x)
{
  std::clog << "'Foo::foo(double x)' is called with x = " << x << std::endl;
  this->foo_(x);
}

I have no precondition to document for this abstract class.

Now, consider a derived class Bar on which a precondition exists to work correctly :

class Bar : public Foo
{
  public:
    /**
     * \brief This function does the job
     * \pre   x must be greater or equal to 0
     */
    virtual void foo_(double x);
};

void Bar::foo_(double x)
{
  assert(x >= 0.0 && "PRE: x can't be negative");
  // Do the job
}

Now, I have a precondition on x when I call foo_, which is called by foo. Then, I have a precondition on foo depending on the final type.

Some questions :

  1. Should I add the precondition in Foo::foo regardless of the final type ? It looks logical if the user never knows the final type when he uses the class. But the user can also have another class Baz derived from Foo without any precondition, and call explicitly Baz::foo(double) with negative values. It shouldn't be a matter.
  2. In my notion of polymorphism, the class Foo doesn't have to know anything about his children, then the precondition can't be there. But the user of the mother class doesn't have to know the children to use the class. How to solve this contradiction ?
  3. Is there a specific(/best) way to document this kind of thing with Doxygen ?

Solution

  • The general rule of using contracts in presence of inheritance is:

    1. Preconditions can only get weaker in descendants.
    2. Postconditions can only get stronger in descendants.

    This guarantees that a client of an ancestor class is not affected by any implementation provided by a descendant, because this client does not necessary know about existence of such a descendant and it should be able to access members declared in the ancestor class freely even if at run-time the corresponding expression is dynamically bound to an object of the descendant class.

    This works fine in most situations. But in your example the ancestor class has a precondition true and the descendant class has a stronger precondition. There are several approaches to deal with that:

    1. Lift the stronger precondition to the ancestor class. Even if implementation there deals with all possible cases, certain scenarios may expect a non-trivial precondition and clients have to deal with that.
    2. Change implementation of the descendant class so that it handles all cases and remove the stronger precondition altogether.
    3. Avoid redeclaration of the member in question either by adding a new method with the strong precondition to the ancestor class or by using a different method in the descendant class, so that it has nothing to do with the original one.
    4. Use abstract contracts. Instead of specifying a contract directly, it can be a call to a virtual function defined in the ancestor class. In your example the function will return true. A descendant class can redefine this function and add the checks it needs to. The main drawback of this solution is that a client will have to check the abstract precondition before it calls the method to make sure it does not violate it.

    The final decision depends on the concrete scenario.