This paper supplies a design and implementation exploration for contracts on function pointers and member function pointers.
The design, in a nutshell, is this: attach a contract onto a pointer declaration. Don't attempt to carry any information through pointer copies at run-time. Mismatched copies are dealt with by a QoI warning.
During the development of C++26 contracts, various audiences requested the ability to have contracts on function pointers.
SG21 looked at a proposal for "function usage types" (P3271). The problem with those is that they aren't function pointers. APIs that want to or have to use function pointers wouldn't be able to be modified to use function usage types.
Various doubts were expressed about whether we can carry the contract information along when function pointers are copied, especially when passing them as parameters.
But we don't need to. Nobody is asking for that. The audiences that have requested contracts for function pointers, and commented on C++26 contracts not being viable for them due to being insufficient, are not asking for runtime conveyance of contract information on function pointers. Said audiences recognize that would be an ABI change we have no reason to think viable.
So the approach taken in this paper is different, simpler, and straightforward: merely attach the contract to a function pointer declaration. Check the contract when a call is made using that function pointer. Declarations of copies of such a pointer may have the same contract, a different contract, or no contract at all. Implementations can supply QoI warnings for the latter two cases.
Callback-taking C-style APIs are the canonical case. The callback type is fixed by the API; the expectations on the callback are not expressible today:
int compare(const void* a, const void* b); int (*cmp)(const void* a, const void* b) pre(a != nullptr && b != nullptr) = compare;
Every call through cmp checks the precondition. When
cmp is handed to an API whose parameter carries no
contract (say, qsort), calls made inside that API are
not checked. That is exactly the point where the QoI warning
described below fires.
A contract can be attached to a function pointer declaration:
void f(int a, int b); void (*p)(int a, int b) pre(a <= b) = f;
and when called, the contract is checked on the call site:
void h() { p(1, 2); // OK p(2, 1); // violation }
A function parameter of pointer-to-function type may also have a contract attached:
void g(void (*p)(int a, int b) pre(a <= b), int a, int b) { p(a, b); } void h2() { g(p, 1, 2); // OK g(p, 2, 1); // violation }
The contract on the pointer declaration governs every call made through that pointer, regardless of which function the pointer currently points to. The pointed-to function's own contract assertions, if any, are evaluated as specified in C++26; the pointer's contract does not replace them. In a call through a pointer with a precondition to a function with a precondition, the pointer's precondition is evaluated first, then the function's. Postconditions are evaluated in the reverse order.
Postconditions work as for functions, including a result name:
int add(int a, int b); int (*fp)(const int a, const int b) post(r: r == a + b) = add;
The C++26 rule that a non-reference parameter odr-used in a
postcondition must be declared const applies to the
parameters of the declarator as well.
A pointer to member function may carry a contract. In the
predicate, this refers to the object expression of
the call, and the cv- and ref-qualifiers of the member function
type apply to it:
struct C { int field; int get(int x) const; }; int (C::*mfp)(int x) const pre(this->field >= 0 && x >= 0) = &C::get; void k(const C& obj) { (obj.*mfp)(1); // checks obj.field >= 0 && 1 >= 0 }
A contract can be attached to the declarator of any of the following, when its type is pointer to function, reference to function, or pointer to member function:
obj.fp(a, b),void (&rf)(int a, int b) pre(a <= b) = f;,template <class R> R (*pf)(R x) pre(x >= 0);
The contract belongs to the declaration, not to the type.
decltype(p) is void(*)(int, int); there is
no overloading on, deduction of, or conversion between contracts.
This is what keeps the design ABI-neutral.
Since copies don't convey contracts, an implementation can warn when a copy loses or changes one. The implementation (see below) warns when a function or function-pointer value is copied (by initialization, assignment, or as a function argument) and:
It does not warn when an uncontracted function is stored into a contracted pointer, since the destination's contract governs. No attempt is made to reason about whether one contract implies another; only exact matches are considered equivalent. An explicit cast to the destination's type silences the warning.
int f(int x) pre(x > 0); int (*fp)(int x); // no contract of its own fp = f; // warning: f's contract is not enforced through fp
The design is implemented in a GCC branch, including all of the
declaration kinds listed above, member function pointers with cv- and
ref-qualifiers, templates, and the
-Wfunction-pointer-contract-mismatch warning.
A runnable example of the examples in this paper, using the
"x86-64 gcc (P4324 contracts, and profiles)" compiler on Compiler
Explorer, is available at
https://godbolt.org/z/5PYr5orfe.
It shows precondition violations reported for calls through a
contracted pointer variable, a contracted parameter, a contracted
member function pointer, and two specializations
(pf<int> and pf<double>) of a
contracted variable template, and the mismatch warning for copying a
contracted pointer into an uncontracted one.
typedef or an alias-declaration.The same approach extends beyond pointers. The implementation also
allows attaching a contract to the declaration of an object of class
type with a function call operator, such as a function object, a
lambda, or a std::function; the contract is checked on
every call through that object. On a function object, the contract
specifier must be written with angle brackets, as
pre<>(...) or post<>(...); plain
pre(...) is not accepted there. The empty angle brackets
are required to disambiguate parsing: a contract on a function pointer
follows a function declarator's parameter list, where pre
and post can only be contract specifiers, but a function
object's declarator has no parameter list, so without the angle
brackets pre and post after the declarator
would be indistinguishable from ordinary identifiers. The parameter names must
be introduced by a binder list; the names of the function call
operator's own parameters are not visible to the contract:
struct adder { int operator()(int a, int b) const; }; adder add pre<>(a, b: a > 0 && b > 0); std::function<int(int, int)> fn pre<>(x, y: x <= y);
A binder list names the parameters in order. In a postcondition of a callable with a non-void return type, the first name introduces the result and the remaining names the parameters; with a void return type, there is no result to name and the list names only the parameters:
adder checked post<>(r, a, b: r == a + b); // r: the result; a, b: the parameters std::function<void(int, int)> g post<>(x, y: x <= y); // void: no result name std::function<int(int)> half post<>(r: r >= 0); // a single name is the result
A single name in a postcondition introduces the result if the callable returns non-void, as in C++26. If the callable returns void and has exactly one parameter, there is no result to name, and the single name names that parameter instead:
std::function<void(int)> sink post<>(x: x >= 0); // void: x names the parameter
A result name on a function returning void is ill-formed in C++26, so this rule gives meaning only to a previously ill-formed form.
This paper proposes neither contracts on function objects nor binder lists; they are mentioned only to show that the approach generalizes.
To be supplied.