This paper supplies a design and implementation exploration for configurable contracts on virtual functions.
During the development of C++26 contracts, there was a long debate on whether contracts on virtual functions should follow a Design-by-Contract (DbC) model, in which an overriding function inherits the contract of the function it overrides and may only weaken its preconditions and strengthen its postconditions, or the model that is now adopted into the C++29 working draft (P3097).
Arguments and examples have been presented for why the DbC model is unsuitable for some users, and for why the other model is. Contracts on virtual functions were removed from C++26 over these concerns. P3097 brought them back into the C++29 working draft with the same model, without addressing the concerns that caused their removal. Neither model satisfies everybody.
This paper, and the prototype implementation backing it, takes a different approach. The P4324 model already makes aspects of contract checking configurable through control objects; this paper makes the inheritance of contracts configurable in the same way, and lets the control object decide. Users then aren't stuck with one model that some of them will find unsuitable. Had P3097 addressed the concerns that led to virtual-function contracts being removed from C++26, this paper wouldn't be needed. Instead, it accepts that some audiences have taken the P3097 model as far as the C++29 working draft, and tries to remedy the situation by making the inheritance semantics configurable through a control object.
In P4324, every contract assertion has a control object: a
constexpr object whose type answers compile-time queries
about the assertion, such as whether it is ignored, and whose function
call operator handles its evaluation and any violation. A contract names
its control object in angle brackets, as in
pre<ctrl>(x > 0). Every query is a static
constexpr member function taking a
std::contracts::assertion_static_info, and an absent
query has a default.
Two mechanisms are proposed. Together, they can express the C++29-adopted model, wholesale inheritance of a base's contract, and the DbC model, and they can be chosen per contract.
inherited()A control object type may declare the query
static constexpr bool inherited(std::contracts::assertion_static_info);
If a virtual function's contract has a control object for which
inherited() returns true, an overriding function
that declares no contract of its own behaves as if it had declared that
same contract, with the same control object. If the query is absent, it
returns false: the override gets no contract, as in the
C++29-adopted model. The details:
info.side()), so a control object can
choose inheritance on only one of them.inherited() for
a side, the program is ill-formed. The proposal is flexible about
that: it has been suggested that, rather than being ill-formed, a set
whose control objects disagree should be combined, and this paper is
open to that.Derived* where the base function returns
Base*), an inherited postcondition sees the override's
result converted to the base function's return type, as a call
through the base function sees it: adjusted to the base subobject,
with a null pointer staying null. The same holds for a reference
return, and for base_contract<Base>() in a
postcondition.base_contract<Base>()An override's own contract can refer to the corresponding contract of a
named base class with std::contracts::base_contract<Base>(),
combined with ordinary operators. This is what expresses DbC
subcontracting: a weaker precondition is
base_contract<Base>() || extra, and a stronger
postcondition is base_contract<Base>() && extra.
The rules:
Base must be an accessible, unambiguous base class whose
function is overridden by the function the contract is on.base_contract may only be used in a precondition or
postcondition of a virtual function override; in a precondition it
stands for the base function's preconditions, in a postcondition for
its postconditions.true, so a composed expression doesn't require every
ancestor to declare one.inherited(): it is always an
explicit reference written by the author of the override.inherited(), and overrides that don't use
base_contract. This is the default.inherited() returns true.base_contract<Base>() || ... and strengthen
postconditions with base_contract<Base>() && ....The examples use a control object that reports violations and continues instead of terminating:
namespace sc = std::contracts; template <bool Inherit> struct report { const char* what; static constexpr bool is_ignored(sc::assertion_static_info) { return false; } static constexpr bool constify(sc::assertion_static_info) { return false; } static constexpr bool assumable(sc::assertion_static_info) { return false; } static constexpr bool inherited(sc::assertion_static_info) { return Inherit; } void operator()(const sc::assertion_context& ctx) const { if (!ctx.check()) std::cerr << " violated: " << what << '\n'; } };
inline constexpr report<false> not_inherited{"Shape::rotate's precondition"}; inline constexpr report<true> inherited{"Shape::scale's precondition"}; struct Shape { virtual void scale(double f) pre<inherited>(f > 0) {} virtual void rotate(double deg) pre<not_inherited>(deg >= 0 && deg < 360) {} virtual ~Shape() = default; }; struct Circle : Shape { void scale(double) override { } // inherits f > 0 void rotate(double) override { } // inherits nothing }; Circle c; Shape& s = c; c.scale(-1); // violation: Circle::scale has inherited f > 0 c.rotate(-1); // no check: Circle::rotate has no contract s.rotate(-1); // violation, checked by the caller: the call names Shape::rotate
struct client_only { // is_ignored, constify, assumable and operator() as above static constexpr bool inherited(sc::assertion_static_info info) { return info.side() == sc::assertion_check_side::client; } }; inline constexpr client_only client_only_v{}; struct Base { virtual int f(int x) pre<client_only_v>(x >= 0) { return x; } virtual ~Base() = default; }; struct Derived : Base { int f(int x) override { return 2 * x; } }; Derived d; d.f(-1); // one violation, reported by the caller; Derived::f itself checks nothing
class Reader { int limit = 100; // private: the base's contract can still use it public: virtual int read(const int n) pre<pre_v>(n > 0 && n <= limit) post<post_v>(r: r >= 0 && r <= n) { return n; } virtual ~Reader() = default; }; struct EvenReader : Reader { int read(const int n) override pre<pre_v>(sc::base_contract<Reader>() || n == 0) // weaker: also accepts 0 post<post_v>(r: sc::base_contract<Reader>() && r % 2 == 0) // stronger: r is also even { return n & ~1; } }; EvenReader e; e.read(0); // OK: the weakened precondition accepts 0 e.read(1000); // violation: neither the base's precondition nor n == 0 holds
Note that EvenReader's contract refers to
Reader's, which uses the private member limit.
That works because the base's contract is evaluated with the base's own
access, as described below.
The implementation never copies the base's contract condition into the derived class. A base's condition may use members private to the base, to which the derived class has no access, and shouldn't need any. Instead:
this-adjusting conversion to the base. It is a
call, not a copy. base_contract<Base>() is replaced
by the same kind of call, one per contract of the matching kind.Only P4324 contracts, which always have a control object, are allowed on virtual functions in the implementation; plain C++26 contracts on virtual functions remain ill-formed.
The design is implemented in a GCC branch: P4324 contracts on virtual
functions, inherited(), and
base_contract<Base>(), with tests covering one-side and
both-side inheritance, an override's own contract taking precedence,
disagreement within a set, ambiguity across bases, transitive inheritance,
postconditions, covariant return types, private members, side reporting,
and DbC weakening and strengthening chains across three levels.
The examples above are runnable on Compiler Explorer with the
"x86-64 gcc (P4324 contracts, and profiles)" compiler, using
-fcontract-control-objects:
base_contract:
https://godbolt.org/z/fTcsrefbTWhen a contract is checked on both sides, the examples report its violation twice, once by the caller and once in the definition.
To be supplied.