Contracts¶

History¶

  • Design by contract comes from Bertrand Meyer's work on Eiffel
  • Described in his book Object-Oriented Software Construction
  • The work builds on Hoare logic and Dijkstra's predicate transformer semantics
  • Hoare logic is also known as Floyd-Hoare logic, Floyd being Robert Floyd who was Jim King's advisor
  • A contract describes:
    • Operation pre- and postconditions
    • Class invariants
      • Class invariants are postconditions common to all class operations
  • A contract is a Hoare triple
    • Expectation (precondition)
    • Guarantee (postcondition)
    • Maintains (invariants)

Preconditions¶

  • The preconditions of a function are what the function expects
    • Violation of a precondition is undefined behavior
    • A precondition can not be tested
      • Instead we test within the domain of the precondition
    • Some preconditions may be asserted by the function
  • It is not practical to assert all preconditions
    • Examples of preconditions which are impractical to test
      • A pair of pointers specify a valid range
      • A comparison function defines a strict-weak-ordering
  • When writing test cases, consider the inflection cases for representative values

Example operator[] and at()¶

  • vector::operator[] has strong preconditions
    • If the index is out of range, behavior is undefined
    • You cannot test behavior for an out-of-range index
  • vector::at() has weaker preconditions
    • If the index is out of range it will throw std::out_of_range
    • The boundary between an index within the range and one outside is an inflection point
{
    vector<int> x{0, 1, 2};
    REQUIRE_THROWS_AS(x.at(2), std::out_of_range);
}
{
    vector<int> x{0, 1, 2};
    REQUIRE_THROWS_AS(x.at(3), std::out_of_range);
}

Exercise 3.1 Write a table with representative values and expected results and a test for indexing.

Basic Interface Preconditions¶

  • The basic interface is the implicit contract which goes without saying
    • So much so that the standard doesn't fully specify the basic interface
  • There are the obvious basic preconditions
    • You can't pass arbitrary memory cast to a particular type to a function
    • The heap can't be corrupt
    • There is sufficient stack space
  • There are also aspects that are more subtle
{
    vector<int> x = { 0, 1, 2, 3 };
    cout << "size: " << x.size() << ", capacity: " << x.capacity() << endl;;
    x.push_back(x.back()); // OK?
    for (const auto& e : x) cout << e << " ";
}
  • The signature for vector::back() is:
T& back();
  • The signature for vector::push_back() is:
void push_back(const T&);

The expected preconditions of a const T& argument to a function, which may alias a value being modified by the function, is:

  • The argument is valid at the invocation
  • It is the called functions responsibility to copy, if necessary, to avoid problems from aliasing
  • This is a weak precondition
  • This is the same situation as self assignment
a = a; // must be a no-op

Exercise 3.2 Extend the assignment test to validate self assignment for representative values.

A T&& argument is more complex:

{
    vector<string> x = { "Hello", "World" };
    cout << "size: " << x.size() << ", capacity: " << x.capacity() << endl;;
    x.push_back(move(x.front())); // OK?
    for (const auto& e : x) cout << e << " ";
}
  • The signature for the vector::push_back() overload in this case is:
void push_back(T&&);

Should this work?

Postconditions¶

  • The postconditions of a function guarantees properties of the result
    • Postconditions can be tested
    • But you cannot test what is not guaranteed
  • When testing a function try to be sure you cover all of the post conditions

Example clear()¶

  • vector::clear() has the following postconditions:
    • removes all elements from the container
    • leaves capacity() unchanged
{
    vector<int> x = {0, 1, 2, 3};
    auto n = x.capacity();
    x.clear();
    REQUIRE(x.empty());
    REQUIRE(x.capacity() == n);
}

Exercise 3.3 Review the postconditions for your existing tests and make sure your tests are complete.

Basic Interface Postconditions¶

  • The basic interface also includes post conditions

Lifetime of reference results¶

  • A member function returning a reference to a part of the object is valid until:
    • a mutating (non-const) member function call
      • Note, that a non-mutating call might not be declared const
      • i.e. vector::begin()
    • or, the end of the objects lifetime

Unsequenced modification and conflicting postconditions¶

  • A classic interview test:
    • What does this print:
{
    int i = 0;
    i += i++ + ++i;
    cout << i << endl;
}
  • The postconditions of move assignment for vector are:
    • The lhs is equal to the prior value of the rhs
    • references, pointers, and iterators to elements in the rhs remain valid
      • but refer to elements that are in lhs
    • The state of the rhs is "valid but unspecified"
      • but because of the above requirements, this usually means "empty"
{
    vector<int> x = {0, 1, 2, 3};
    x = move(x);
    cout << x.size() << endl;
}
  • The only state that could satisfy the documented postconditions for move assignment with self move are a no-op
  • Compare to unique_ptr
    • move assignment is as if by calling reset(r.release())
    • this implies that a self move is x.reset(x.release())
      • which is a no-op
{
    auto x = make_unique<int>(42);
    x = move(x);
    cout << *x << endl;
}
  • The postconditions on moving a string are:
    • lhs contains the prior rhs value
    • rhs value is "valid but unspecified"
  • Until C++17 the postcondition of self-move on a string was:
    • "the function has no effect"
  • But this language was removed in C++17
{
    string x = "Hello";
    x = move(x);
    cout << x.size() << endl;
}
  • So should this case work from 1.2.2?
{
    vector<string> x = { "Hello", "World" };
    cout << "size: " << x.size() << ", capacity: " << x.capacity() << endl;;
    x.push_back(move(x.front())); // OK?
    for (const auto& e : x) cout << e << " ";
}
  • It is debatable
    • If it works, x.front() is "valid but unspecified"
    • For it to work may require moving a moved from object when vector is resized
    • It requires an additional move to hold the value during reallocation
  • The basic interface is only partially specified
    • aliased references are only discussed with regard to race conditions
    • unless otherwise specified, treat modifying the same object as an unsequenced modification
      • even if specified, be cautious, this is an area of change
  • The issues of aliasing can often be side-stepped by passing sink arguments by value
  • i.e. if the signature f push_back() was:
void push_back(T);

One argument is that for the basic interface, passing arguments by rvalue and lvalue references should be viewed as an optimization of passing by value and should not change behavior.

But that has performance implications.

Exception Guarantees¶

  • The exception guarantees are part of the basic interface
  • They describe the postconditions of a function in the event of an exception
    • There are 4 levels, from strong to weak
    • noexcept: Will not throw an exception
    • strong: Any modified state is returned to its prior, logical state
    • basic: All modified objects are left in a "valid but unspecified" state
    • weak: Result is undefined
  • Unless otherwise specified, the basic guarantee is assumed
  • In the absence of a modification, the basic and strong exception guarantees are the same.
  • For example:
{
    vector<int> x{0, 1, 2};
    auto copy = x;
    REQUIRE_THROWS_AS(x.at(3), std::out_of_range);
    REQUIRE(copy == x); // per basic exception guarantee
}

Invariants¶

  • An invariant is a relationship which must hold irrespective of the operation performed
    • They are a generalized collection of postconditions and as such are testable
template <class T>
void test_vector_invariants(const T& x) {
    REQUIRE(!(x.capacity() < x.size()));
    REQUIRE((x.size() == 0) == x.empty());
    REQUIRE(x.empty() == (x.begin() == x.end()));
    //...
}

Exercise 3.4 Complete the invariant test for a vector and extend your tests to check the invariants after each mutating operation.

Security¶

  • A secure interface has no preconditions
  • A secure system has no bugs
    • To exploit a system:
      • Identify interfaces which cannot be verified
      • Boundary conditions that may not have been anticipated