2026-04-08
preliminary discussion for possible integration into IS ISO/IEC 9899:202y
| document number | date | comment |
|---|---|---|
| n3739 | 202511 | Original proposal |
| n3789 | 202601 | Retire the “syntactically equivalent” concept |
| Require that at most one declaration has contracts | ||
| Use “ghost state” instead of “ghost variable” | ||
| Add invariants as composition of pre- and postconditions | ||
| Simplify the example | ||
| Add more examples | ||
Make _ReturnValue visible
in preconditions |
||
| Add implicit conversion | ||
| n3867 | 202604 | Remove stdc_contract_assert and
stdc_contract_assume |
| Remove attributes and make the macros function-like | ||
| Remove the configuration macros | ||
| Move contract list to the end of a full declarator | ||
| Make the rule on compatible types consistent with the rest | ||
| Make the appearance of side effects implementation-defined | ||
Add const-qualification
of parameters |
CC BY, see https://creativecommons.org/licenses/by/4.0
Traditionally, C’s function interfaces are particularly poor in the way information that can be transferred from the calling context into the caller and even poorer for the way back from the called function back to the caller. Originally, C only accounted for the number of arguments of a call and the return type, the addition of prototypes to the language added more static information about parameter types. This has later be enriched with only some more bits of dynamic information, in particular array sizes.
The goal of this paper is to continue this process of improvement of interface specifications to the dynamic verification of predicates which the current syntax is not capable to warrant. Algorithmic invariants or properties that are needed for optimization are not expressible in the interfaces that the language currently offers and, in general, are either missed, annotated manually with external tools, or, most commonly, by handwritten textual proofs.
Nevertheless, many predicates that would be needed here can be
written as C expressions, provided that we add some additional state
that, without interacting with the core of the computation, helps to
propagate properties forward. The goal of this paper is to show how
function interfaces in C can be enriched such that necessary predicates
for calling them and conclusions from a call can be naturally expressed
in otherwise existing C syntax and semantics. The core argument of this
paper is to show that this execution model, with reasonable assumptions
on the acceptable predicates, is equivalent to an implementation in
conventional C where shallow inline wrappers execute cascaded if statements to
check the predicates.
The goal of this paper is not to provide methods for hardening existing interfaces, and, in particular, the C library. In the contrary, adding contracts as proposed here changes the semantics of interfaces (in general from UB to well-defined failures) and they are thus not suited to paint over existing deficiencies in interface design.
Numerous tools exist in the field that ensure the check of
predicates, usually expressed either as assertions or as pre- or
postconditions. In the following we discuss three of them that have had
the most influence on this proposal, here. The main motivation came from
a similar feature that is a candidate to make it into C++26, C++
contracts; others are the traditional assert macro and Annex K of the C
standard.
C++ contracts are a complicated feature, in particular they let
implementations chose between different semantics of a
translated contract (ignore, observe, enforce
and quick-enforce). Additionally, for two of these semantics
(observe and enforce) they allow to configure behavior
in the case of a contract violation by means of a user-supplied contract
handler, ::handle_contract_violation.
We don’t think that this whole spectrum of possibilities is adequate for
a C feature.
First, for C it is not appropriate that readily compiled translation units (such as the C library or other third-party libraries) change behavior by means of a user-supplied contract handler. Such a global contract handler has similar effects as the runtime-constraint handler that is proposed by optional Annex K; for various reasons such a runtime-constraint constraint handler was not accepted by large parts of the C community as an appropriate tool and the annex is only implemented by very few implementations.
Second, the ignore semantics are counter-productive as they
inhibit the evaluation of the predicate, and thus change semantics even
in case that a contract holds. Thereby, the ignore semantics
partially repeat a design error that was already present for C’s assert feature, see below.
Third, C++ contracts allow the predicates to have side effects, which then are possibly elided. In C we do not have that concept, and so allowing side effects makes it difficult to argue about semantics of contracts.
As a consequence our proposal with its default behavior only is compatible with C++’s quick-enforce semantics, and this only if the contract has no side effects. In particular, as proposed here for C, an optimizing compiler will be allowed to assume that the predicate holds unconditionally in all code that is sequenced after the evaluation. But, in contrast to earlier versions of this proposal, side effects are allowed; only they may appear out of sequence and up to two times (issued by the caller and by the callee). The details of such evaluations of side effects are implementation-defined.
Other features in our proposal that are different from C++ contracts:
if-statements._ReturnValue and
cannot be annotated with attributes._Pre, _Post and _Inv for contracts.
C++’s syntax with pre and post (plus inv) is available
with function-like macros. Contracts cannot be annotated with
attributes.stdc_contract_terminate that in general
leads to the printing of a diagnostic and then to program
termination.Our proposed feature is meant to be compatible to C++ contracts at least for the quick-enforce semantics and in the absence of side-effects and contract attributes. We impose that the strategy in case of a contract violation is statically determined for each pre- or postcondition; the default behavior is that the execution (of the program or thread) terminates. In any case, a C proposal for contracts cannot have exactly the same syntax as for C++, because C++ uses syntactic concepts that are not available in C. Nevertheless, we try to provide a syntax that can be mapped between the languages by using some macro glue.
In particular, the difference in the syntax for the name of the return value could be simply avoided in C++ by using a definition such as
#define post(...) post(_ReturnValue: __VA_ARGS__)This then forces that a return value of the name that C understands is defined, and that a post condition that has the C++ syntax for return values results in a syntax error.
assert C library macroHistorically, C also has a macro that is intended to diagnose the
violation of a given predicate, assert
from clause 7.2 “Diagnostics <assert.h>”.
Unfortunately this macro has not aged well and lacks capacities that
would be needed in a modern environment of translators with improved
optimization capabilities or even with integrated static analyzers. The
main defect of that macro is that it is switched off as a whole by the
user defined macro NDEBUG and that
thus the resulting executables have different syntax and semantics:
assert invocation may inadvertently be
invalidated by distant changes of the source.NDEBUG is set:
NDEBUG.NDEBUG continues an erroneous
execution if the predicate does not hold. This may lead to undefined
behavior somewhere down the line.These semantic defects seem to be shared with C++’s ignore semantics.
Another issue with assert is that
it uses abort to terminate the
execution. Leaving invocations of assert without using NDEBUG is problematic:
abort passes through the signal
handling mechanism (with the signal SIGABRT) and is catchable.
exit or quick_exit handlers are not run.One goal of this paper here is to provide a better tool that in the
future could replace uses of assert
that are meant what the name verbally indicates, namely where the users
seeks to assert a specific property. On the other hand, uses of assert as a diagnostic tool (and which goal
is indicated by the clause header of 7.2 “Diagnostics”) would and should
not be replaced.
For the C library, Annex K has already taken the approach to replace the existing C library interfaces by new variants that make more guarantees and that introduce runtime checks for constraints. This can be seen as a case-study to amended specific C library interfaces. What are called runtime constraints, there, has a lot of similarities with predicates as defined in this paper.
Our approach differs from Annex K in multiple ways
As it is the common model in C, we will assume that the functions that make up a program are split in translation units, TU, that each correspond to a C file that is compiled separately from all the others. A TU knows about functions in another TU only by declarations, most properties of these other functions are hidden. The advantages of such an approach are plenty, for example modularization, maintainability and fast compilation times, but there are also severe disadvantages: false assumptions lead to complicated bugs and lack of information misses optimization opportunities.
The proposed model for adding contracts to C interfaces is relatively simple at the surface: we create the possibility to add a list of contracts (pre-, postconditions and invariants) to function declarations at the very end. Contracts can effectively be checked from the calling context of a function or within the function itself (or both) without leading to semantic differences. The intent is that in general for preconditions the check is effected in the caller and for postconditions in the callee, such that these checks may seamlessly use knowledge that is already present in these contexts.
We took care that contracts as proposed here
There is a multitude of possible design choices for contracts, but for us, when restricted to the context of C, several choices seemed to impose themselves.
Our main choices for the feature are as follows; changing any of these substantially would possibly invalidate the approach as a whole.
A crucial property of contracts is, that, in general, a violated
predicate terminates execution. As of today, after more than half a
century of C, there is still no established strategy on how to terminate
an erroneous execution; the C standard alone has 6 different features
(exit, quick_exit, _Exit, thrd_exit, abort and signals). Indeed, depending on the
field, requirements for such termination are very different; in some
contexts a graceful exit with cleanup is completely adequate; others
should never terminate at all (such as an operating system); some cannot
assure any consistent behavior after a violation and should halt
instantly without any cleanup.
For these reasons we left the specific choice how to terminate by default to the implementation. They are best placed to offer the necessary tools and local choices to their customers.
Nevertheless all implementations have to provide a default behavior. This mode is particularly important for providers of libraries that cannot afford that user code (or other libraries) impose a different termination strategy on them. Implementations may want provide fine-tuning for this policy by means of command line arguments or configuration macros; such a specification is willingly left out of the proposed text.
Different declarations of the same function with contracts within the same TU would raise the question of consistency between those declarations. An early version of this paper attempted to provide a mechanism that would enforce consistency, but was considered too complicated, at least for an initial specification of such a feature.
Now we only require simpler properties. First, function parameters of
different declarations for the same function with contracts must have
the same name (if any). Second, at most one declaration may be annotated
with contracts, such that a contradiction can simply not occur. For
functions that are defined before possibly being redeclared (in general
static or
inline
functions) contracts should be specified there; for functions that are
forward declared in headers and then only defined in one TU, the
contract is assumed to be at the forward declaration.
Within the same TU, function types with contracts can only be
compatible if they are the same, that is if they refer to the same
function type declaration; no redeclaration with contracts of a function
type that already has them leads to a compatible type. Thereby function
pointers to functions with contracts can only be interchanged if they
refer to the same original type, and not a redeclaration of a compatible
type. In cases where there is need for such a design, forward
declarations with contracts should refer to a common type either by
using a typedef:
typedef void* allocation_type(size_t n) _Post(_ReturnValue);
allocation_type my_malloc;
allocation_type my_arena;
allocation_type* what_can_we_do_for_you_today = my_arena;or by using typeof to avoid a
redeclaration
void* my_alloc(size_t n) _Post(_ReturnValue);
typeof(my_alloc) my_arena;
typeof(my_alloc)* what_can_we_do_for_you_today = my_arena;Between different TU, ensuring consistency is difficult. The overall idea is that for functions that are called from different TU, the contract is specified in a declaration in a header file, and that that specification is precise enough such that no ambiguity occurs.
In general, such consistency cannot be guaranteed without additional mechanisms and tools. Therefore our specification only forsees UB in case that during an execution evaluations of parameter types or contracts would lead to different results. Additionally, we require that the number and kind of contracts (pre-, postcondition or invariant) agree, and that for any call of the function the evaluation of the contracts has the same result.
For parameter types the UB was already present, though implicitly. For contracts this is a new UB, but which we think is unavoidable for current C.
It is possible to put stronger mechanisms in place that would ensure that contracts between different translation units are consistent. Our reference implementation (see below) for example does this by encoding all contracts into the name of the auxiliary function as hashs; thus only translation units with consistent contracts can be linked into one executable. But since there is no commonly agreed mechanism for this, we leave it to the quality of implementation for the moment.
in p4
… If the declarator or type specifier that declares the identifier appears within the list of parameter declarations in a function prototype (not part of a function definition), the identifier has function prototype scope, which terminates at the end of the innermost full declarator that contains the function declarator or the end of the innermost full abstract declarator that contains the function abstract declarator. …
8 A pointer to a function of one type can be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer. If a converted pointer is used to call a function whose type is not compatible with the referenced type or if a function without contracts is called with a type with contracts, the behavior is undefined.
add _Inv,
_Post and
_Pre to the
list of keywords.
Add items in p1 after the second position
— the left operand has atomic, qualified, or unqualified pointer to a function type without contracts, and (considering the type the left operand would have after lvalue conversion) both operands are pointers to compatible types;
— the left operand has atomic, qualified, or unqualified pointer to a function type with contracts, and (considering the type the left operand would have after lvalue conversion) and the right operand has the same type as the left operand;
and amend the now fifth item
— the left operand has atomic, qualified, or unqualified pointer type, and (considering the type the left operand would have after lvalue conversion) both operands are pointers to qualified or unqualified versions of compatible object types, and the type pointed to by the left operand has all the qualifiers of the type pointed to by the right operand;
Apply the following changes.
Add contract-list to the syntax in 6.7.7.1
declarator:
pointeropt direct-declarator contract-listopt
[…]
Constraint
1′ If a contract list appears in a declarator, that declarator shall be
a full declarator, there shall be a function type T such that the declared type is
T or a pointer to T, and, before possible replacement
of array length expressions by the token *, no type of a
parameter of T shall have a
type component of unspecified array length.
and in 6.7.8
abstract-declarator:
pointer contract-listopt
pointeropt direct-abstract-declarator contract-listopt
[…]
Constraints
1′ If a contract list appears in an abstract declarator, that abstract
declarator shall be a full abstract declarator, there shall be a
function type T such that the
declared type is T or a
pointer to T, and, before
possible replacement of array length expressions by the token *, no type of a
parameter of T shall have a
type component of unspecified array length.
Semantics
2 In several contexts, it is necessary to specify a type. This is accomplished using a type name, which is syntactically a declaration for a function or an object of that type that omits the identifier.144) A full abstract declarator is an abstract declarator that is not part of another abstract declarator. The optional attribute specifier sequence in a direct abstract declarator appertains to the preceding array or function type. The attribute specifier sequence affects the type only for the declaration it appears in, not other declarations involving the same type.
add a new paragraph to the Constraints section
Constraints
[…]
4′ Where a composite type of two compatible function types that are declared in the same translation unit is required, at most one of them shall have a contract list.
Then, in the Semantics section
Semantics
[…]
15 Two function types without contract list are compatible if and only if all of the following hold:
- They specify compatible return types.
- The parameter type lists agree in the number of parameters and in whether the function is variadic or not.
- The corresponding parameters have compatible types.
In the determination of type compatibility and of a composite type, each parameter declared with function or array type is taken as having the adjusted type and each parameter declared with qualified type is taken as having the unqualified version of its declared type.
15′ Unless further restricted hereafter, two function types, where at least one is declared with a contract list, are compatible if they are compatible when the contract lists are omitted and if for each corresponding pair of direct declarators of parameters, if any, the declared identifiers are the same.Ftt) Additionally, if both are declared with a contract list,Fuß) for the function types to be compatible the two contract lists shall have the same number of contracts and each contract of a corresponding pair of contracts (in declaration order) shall be of the same contract specifier.
Ftt) This condition for a parameter name only applies if both function types name the parameter; if at least one of them uses a direct abstract declarator instead of a direct declarator it does not name the parameter and thus there is no condition to fulfill.
Fuß) No two such function declarations for an entity appear in the same translation unit without violating a constraint (6.7.7.4.1).
15′′ For two compatible function types E and F that are declared in the same translation unit,
- where E is with a contract list,
- where E− is the type E without contract list,
- and where F is without a contract list,
the composite type of E and F is the composite type of E− and F
- which has all the parameter names of F and E combined
- where all parameter types are
const-qualified,- and to which the contract list of E is added.
6.7.7.4.1 Contracts
Syntax
1 contract-list:
contract
contract-list contract
contract:
contract-specifier
(selection-header)
contract-specifier: one of
_Pre_Post_Inv
Description
2 Contracts are annotations for function declarators that check specified predicates and that declare a number of internal states that can be used to ensure the consistency of an execution. The intent is to make such contracts visible simultaneously for the call side of a function and its definition, and, to restrict array length expressions of function parameters and predicates such that evaluating them on either side (caller or callee) leads to the same result. Thereby contracts open the possibility to verify programs and their executions, and to provide optimization opportunities, even if the program is composed of several translation units. Contracts formulate requirements that are transferred from the calling context into the function context (preconditions, indicated by the keyword
_Pre) and others that are transferred back from the call into the calling context (postconditions indicated by_Post). Invariants (indicated by_Inv) combine a pre- and a postcondition as described later in this subclause.
3 The predicate of a contract is a logical condition specified as an expression of scalar type; the expression that forms the predicate is determined by the selection header according to the same rules as for the controlling expression of an
ifstatement (6.5.8.2). The predicate is evaluated each time a call to the function is evaluated; a contract is violated if the predicate compares equal to0. If the selection header includes a declaration, the declared objects are called ghost states; the declared identifiers have no linkage and a scope of visibility that starts with the declaration and ends with the end of the function prototype scope or the block scope of the function body, respectively. The life time of a ghost state is the whole execution (if declared withstatic), the current thread of the function call (if declared withthread_local), or the function call including the evaluation of all pre- and postconditions, otherwise. They are visible to all following contracts in the list. If not yet present, the declared type is implicitly amended with aconstqualifier. If the initializer of such a declaration is an integer constant expression, the identifier is a named constant.
4 Additionally, if the return type of the function is not
void, an additional ghost state is provided by the predefined identifier_ReturnValuewhich evaluates to a non-modifiable lvalue of theconst-qualified return type that holds the return value of the function during all evaluations in a postcondition; the underlying object is unique for each function call, has automatic storage duration and has a lifetime that spans the function call. It is visible in all pre- and postconditions, but only accessible in postconditions.Foot)
Foot) Thus
_ReturnValueis even usable in preconditions for constructs such astypeof_unqual(_ReturnValue). It is recommended that implementations diagnose an access to the underlying object in a precondition.
5 The type of all parameters of a function with contracts is
const-qualified, maybe implicitly. Within a contract, the name of a parameter evaluates to the value as provided by the corresponding argument to the call after it has been converted to the adjusted type of the parameter.
Constraints
6 A predicate shall have scalar type and if it is an integer constant expression it shall not have the value zero. The selection header of an invariant shall only have the first form. All identifiers used in a contract shall have visible declarations and, unless they are named constants, shall not refer to objects or functions with internal linkage. All ghost states shall have an initializer and a type that is neither an atomic type nor is
volatilequalified; if the function declaration is also a definition, the declared identifier shall not be used inside the compound statement of the function body.fft)
fft) This not withstanding a redeclaration of the same identifier in an inner block of the function body is valid.
Rationale: predicates that are known to be always
false should abort compilation with semantics similar to static_assert.
Rationale: contracts are primarily thought as annotations of an interface between different translation units. Therefore they should not read the contents of static variables or call static functions. They must not have UB if a ghost state would be uninitialized in some situations.
7 If a translation unit contains the definition of a function
Fand anywhere in the translation unit there is a declaration ofFwith contract list, that declaration shall be visible in the position of the definition ofF; such a function is said with contracts. Where the composite type of two function types is required, at most one of the types shall have a contract list.FTT)
FTT) This means in particular that if there are several declarations of a function (including a definition) or of a function pointer, at most one of them has a contract list.
Rationale: The definition must always implement the contracts for the case that it is called through a function pointer.
Rationale: It is difficult to specify conditions in different parts of a TU that would be guaranteed to evaluate to the same value, regardless of the circumstances.
8 The address of a function with contracts shall not be passed as the second argument to a call to
thrd_create.
Rationale: Calling such a function would evaluate
the contracts in a different thread than the visible context of the call
to thrd_create.
Semantics
9 On entry to a function with contracts, after the parameter values and their types have been determined,ftt) after evaluation of array length expressions of parameters and after array parameter adjustment, but before the compound statement of the function body is executed, the predicates of preconditions are evaluated and it is assessed if the preconditions are violated. Similarly, after a
returnstatement has been reached and a possible return expression has been evaluated and converted to the return type, or if the closing brace of the body has been reached, predicates of postconditions are evaluated and assessed. All evaluations and assessments of preconditions are sequenced in the order in which they are specified and within the same thread as the function call; similarly, evaluations and assessments of postconditions are sequenced in specification order.
ftt) Note that the value of a parameter is the value of the corresponding argument after conversion to the type of the parameter.
10 An invariant ensures that the predicate has the same scalar value before and after a function call; if
Eis the predicate expression, it is equivalent to an in-place replacement by two contracts
_Pre(auto AUXID = (E); AUXID)
_Post((E) == AUXID)
where
AUXIDis a unique auxiliary identifier that is not otherwise used in the translation unit.fu)
fu) Note that, depending on the implementation-defined evaluation model, a side effect in
Ecan be evaluated multiple times.
11 If and when a contract is violated, execution terminates as-if by an invocation of the macro
stdc_contract_terminate(string literal)(7.X.Y) with an ordinary string literal argument that contains at least the current expansion of the macros__FILE__and__LINE__. If the macrostdc_contract_terminateis undefined at the position of the specification of the contract, the functionstdc_contract_terminateis called instead.Pied)
Pied) Thus, the behavior of a contract violation is decided in phase 7 if the macro is defined (
<stdc_contract.h>has been included) or in phase 8 if it is not (<stdc_contract.h>has not been included or the macro has been undefined).
12 For a function declaration with contract list, evaluations of array length expressions in parameters and evaluations in contracts behave as follows. They shall not access objects or functions of static storage duration unless these stem from a definition with external linkage, unless they are accessed through a pointer parameter, or unless they are string literals.
13 If the evaluation of an array length expression in a parameter declaration or of a predicate has side effects that are
- determining an array length,
- initializing a ghost state,
- initializing a compound literal, or
- terminating the execution of the program or thread,
they are sequenced consistently among each other according to the specification order, that is:
- array length expression in parameters
- predicates of preconditions
- the execution of the function body,
- predicates of postconditions.
14 For any other side effect of an array length expression in a parameter declaration or of a predicate the behavior is implementation-defined among the following three choices; the side effect either
- is discarded,
- happens exactly once per call,
- or, happens from zero up to two times per call,
depending on linkage, optimization and other implementation choices. Whence such a side effects happens, it is indeterminately sequenced with other evaluations in the current thread.
15 If a function without contract is called with a type that has a contract list, the behavior is undefined.
Rationale: This forbids the compilation of a function without contracts and then adding such contracts later to a header file. If that would be allowed, the function could eventually be called without the check for any of the predicates in place, and thus the semantics of the contracted interface would be violated.
If an external definition of a function with contracts is called with a type with contract list from a different translation unit than the definition, the evaluation of array length expressions of parameter declarations in the context of the function call and of the function definition shall result in parameter types, before array length replacement by
*in parameters and before parameter adjustment, that are pairwise compatible. To be compatible, both function types have the same number of contracts and of the same contract specifier (6.7.7.4); then for each corresponding pair of contracts (in specification order) either both shall be valid or both shall be violated.
Recommended practice
16 It is recommended that implementations diagnose side effects in the evaluation of predicates that permanently change the state of the execution.
17 NOTE A function with contracts may always be called by using a declaration without contracts or by using its address, but such a call may be less efficient than a direct call.
18 EXAMPLE 1 Provided that the implementation has the type
uintptr_tand that the ordering of that type is compatible with the conversion fromunsigned char*the following presents contracts for a functionmy_memcpythat fulfills similar requirements asmemcpy. Other than formemcpy, here, passing overlapping byte arrays as arguments terminates the program execution in a controlled way, no undefined behavior occurs in that case.
void *my_memcpy(void*const dest, const void*const src, size_t const n)
_Pre(n)
_Pre(const unsigned char *const dest_start = dest)
_Pre(const unsigned char *const src_start = src)
_Pre(((uintptr_t)(src_start + n) <= (uintptr_t)dest_start)
|| ((uintptr_t)(dest_start + n) <= (uintptr_t)src_start))
_Post(_ReturnValue == dest);The first three contracts guarantee that the parameters have values that correspond to array objects of strictly positive length. The precondition using the ghost states tests that both byte arrays of length
nthat are passed as arguments do not overlap; note that the requirements here could also be expressed by addingrestrictqualifications, but note also that doing so would not enforce them. Last, the postcondition assures that the function returns the same value as the initial value ofdest.
Information for the range check will in general be present at the point of a call; thus if a translator is able to prove that requirement, the check for it can be omitted. Also, the implementation is able to make the guarantee for the postcondition when translating the definition of the function, so then the check for this postcondition is superfluous. On the other hand, a translator where only the declaration is visible is able to deduce that property from the postcondition and optimize accordingly in the context of the call.
19 EXAMPLE 2 If a functions with contract is defined as inline, the contracts are best placed at the inline definition.
// header "range.h"
#include <stdint.h>
inline
bool range_check(void const*const a,
void const*const b,
size_t const n) [[unsequenced]]
_Pre(a) _Pre(b) _Pre(n)
{
const unsigned char *const dest_start = a,
const unsigned char *const src_start = b;
return
((uintptr_t)(src_start + n) <= (uintptr_t)dest_start)
|| ((uintptr_t)(dest_start + n) <= (uintptr_t)src_start);
}The declaration of the previous example may then be rewritten to use a call to that function as a predicate.
#include "range.h"
void *my_memcpy(void*const dest, const void*const src, size_t const n)
_Pre(range_check(dest, src, n))
_Post(_ReturnValue == dest);Here the same contracts as before are checked recursively in the call to
range_check. In the translation unit that provides the external definition ofrange_check, there is no need to specify the contracts, again:
#include "range.h"
// makes the definition external
extern typeof(range_check) range_check;20 EXAMPLE 3 If the addresses of a set of functions with contracts are to be used interchangeably, it is recommended to use a type definition for the declarations that add the contracts.
// header file "alloc.h"
typedef void* alloc_t(size_t n) _Pre(n) _Post(_ReturnValue);
extern alloc_t alloc_uninit;
extern alloc_t alloc_init;
extern alloc_t* alloc_today;This ensures that both function pointers
&alloc_initand&alloc_uninitare candidates for assignment toalloc_today; since the types are the same (and not only compatible) no type compatibility problems arise between different function pointers.
In the translation unit that implements the functions, definitions without contract list are used:
// implementation
#include <stdlib.h>
#include "alloc.h"
alloc_t* alloc_today =
#if USE_ALLOC‿UNINIT
alloc_uninit
#else
alloc_init
#endif
;
// the composite type has the contracts
void* alloc_uninit(size_t n) {
void* ret = malloc(n);
if (!ret) {
stdc_contract_terminate("out of memory in allocation");
}
return ret;
}
// the composite type has the contracts
void* alloc_init(size_t n) {
void* ret = calloc(n, 1);
if (!ret) {
stdc_contract_terminate("out of memory in allocation");
}
return ret;
}Here, the definitions have types that are compatible and that use the same parameter name
nasalloc_t. The composite type of a type with contract list (from the header) is then formed with a type that has no contract list. Note also, that the postcondition_Post(_ReturnValue)is implicitly checked in both definitions; if the allocation fails, the[[noreturn]]attribute ofstdc_contract_terminateguarantees that thereturnstatement is not reached.
<stdc_contract.h>Remark: It would also be possible to add these macros to <assert.h>,
but without using the NDEBUG
mechanism.
21 The
<stdc_contract.h>header adds macros (stdc_contract_terminate,inv,preandpost) for the contracts feature (6.7.7.4.1).stdc_contract_terminateis described in 7.x.x, the macrosinv,preandpostare defined as if by the following
#include <stdc_contract.h>
#ifdef __cplusplus
# define post(...) post(_ReturnValue: __VA_ARGS__)
#else
# ifndef inv
# define inv(...) _Inv(__VA_ARGS__)
# endif
# ifndef pre
# define pre(...) _Pre(__VA_ARGS__)
# endif
# ifndef post
# define post(...) _Post(__VA_ARGS__)
# endif
#endifstdc_contract_terminate macro and
functionSynopsis
[[noreturn]] void (stdc_contract_terminate)(char const* mess);
typedef void stdc_contract_termination_t(int);2 The
stdc_contract_terminatemacro takes one argument, which is an ordinary string literal, called the message and which provides a diagnostic message.
Rationale: strings are always required to be ordinary string literals, here, to ensure that these strings are valid during all phases of termination of the execution and that no character set conversion has to be performed dynamically. In particular, dynamic allocations of diagnostic strings are to be avoided.
The behavior of an invocation of
stdc_contract_terminateis implementation-defined. A default behavior is provided such that if the position of an invocation is reached during execution:
- The invocation terminates the current function call.
- Then, a diagnostic message is issued on the standard error stream that contains the argument string.
- Finally, the execution of the program or of the current thread is terminated with the status unsuccessful termination (7.25.5.4).
The latter happens as if by a call to a function that is compatible to
stdc_contract_termination_twith the argumentEXIT_FAILURE. It is implementation-defined if one of the function_Exit,quick_exit,exitorthrd_exitis called, or if it is a different implementation-defined function with typestdc_contract_termination_t.Ex) The implementation-defined choice is determined before or during translation phase 7.Link)
Ex) Note that the behavior of calling any of
_Exit,quick_exit, andexitis undefined in the presence of multiple threads of execution.
Link) The behavior of a particular translation unit does not depend on linking.
3 Additionally, there is a function
stdc_contract_terminateof the same name as the macro and with the indicated prototype that is called if macro expansion is inhibited. The behavior of that function is as described for the default behavior only that the particular choice of the function with typestdc_contract_termination_tmay be different; it is determined in translation phase 8 and is the same for all calls to thestdc_contract_terminatefunction of the same program execution. If the external function is called with an argument that does refer to an object that does not have static life time, that has been modified during program execution, or that does not hold a sequence of printable characters that are terminated by a null character, the behavior is undefined.
4 Recommended practice It is recommended that the implementation-defined diagnostic message that is printed before termination also contains the values of
__FILE__and__LINE__as of the point of invocation of the macro.
As defined in the suggested text above, it may seem as if contracts impose severe constraints to all executions, as they assume that all contracts are checked on all function calls. Because of the strong impact that such a behavior would then have for example on execution times, as such this would probably not be a viable proposal.
In fact, modern compilers and static analyzers should be able to prove for many contracts that they hold unconditionally; thus they are dead code and can be removed. One of the keys to that is that all evaluations in contracts are forced to be free of side effects and can in general be hoisted out of the immediate calling context. The other key is the requirement that contracts for the same function, even if specified in different translation units, have to behave the same when presented with the same arguments.
In the following a selection-header of a precondition is given in the form
dec_preᵢ predicate_preᵢwhere the part
dec_preᵢ is a declaration (of one
or several ghost states) in the formtype_preᵢ ghost_pre⁰ᵢ = init_pre⁰ᵢ, ...;predicate_preᵢ is a predicate
that is to be checked.The same syntax as for if-statements is
admissible, that is either dec_preᵢ or
predicate_preᵢ can be empty, but not
both. Both, dec_preᵢ and predicate_preᵢ, are the usual C constructs
as they may appear in if-statements.
If predicate_preᵢ is empty, then
(per definition of the if statement) dec_preᵢ is a simple declaration defining
exactly one ghost state ghost_pre⁰ᵢ
with initializer init_pre⁰ᵢ. predicate_preᵢ is then assumed to be the
expression init_pre⁰ᵢ.
Analogous definitions hold for postconditions in the form dec_postᵢ predicate_postᵢ.
In the following we discuss three different views of a function with contracts,
If a function with contracts is given as an inline definition, the
semantics are relatively simple and easily explained in existing C
syntax: tests for the preconditions have to be inserted before the rest
of the function body and tests for post conditions afterwards. There is
only one subtlety, namely the handling of return statements.
In fact, all return statements
are replaced by assignments to the auxiliary variable _ReturnValue and
then a proper return statement is
added at the end, after all postconditions have been checked.
Suppose a list of contracts is attached to a function definition with
inline:
// header
inline returnType func(parameterTypeList)
pre(dec_pre₀ predicate_pre₀)
pre(dec_pre₁ predicate_pre₁)
…
pre(dec_preₙ predicate_preₙ)
post(dec_post₀ predicate_post₀)
post(dec_post₁ predicate_post₁)
…
post(dec_postₘ predicate_postₘ)
function-body// translation unit
typeof(func) func; // instantiation of funcThe effect is as-if the function was augmented with cascaded if statements that
test for the conditions.
// header
inline returnType func(parameterTypeList) {
returnType _ReturnValue;
if (dec_pre₀ !predicate_pre₀) {
static_assert(ICE_OR_TRUE(predicate_pre₀), "some string");
stdc_contract_terminate("some string");
} else if (dec_pre₁ !predicate_pre₁) {
static_assert(ICE_OR_TRUE(predicate_pre₁), "some string");
stdc_contract_terminate("some string");
} else if (…) {
...
} else if (dec_preₙ !predicate_preₙ) {
static_assert(ICE_OR_TRUE(predicate_preₙ), "some string");
stdc_contract_terminate("some string");
} else {
_Defer {
if (dec_post₀ !predicate_post₀) {
static_assert(ICE_OR_TRUE(predicate_post₀), "some string");
stdc_contract_terminate("some string");
} else if (dec_post₁ !predicate_post₁) {
static_assert(ICE_OR_TRUE(predicate_post₁), "some string");
stdc_contract_terminate("some string");
} else if (…) {
...
} else if (dec_postₘ !predicate_postₘ) {
static_assert(ICE_OR_TRUE(predicate_postₘ), "some string");
stdc_contract_terminate("some string");
}
}
replaced-function-body // amend return with assignment
}
}Here, replaced-function-body is the function-body
where all expressions in a return statement of
the form
return expressionare replaced by assignments
return (_ReturnValue = expression)_Defer is
the defer feature as described in the defer technical specification, and
the macro ICE_OR_TRUE is supposed to
resolve to its argument if that is an integer constant expression (ICE)
and to true,
otherwise.
Note that the if-statements with
the inverted condition are such that all definitions of ghost states in
the form dec_preᵢ or dec_postᵢ are visible to all evaluations in
the subsequent contracts.
The derived inline function definition has the following properties.
static_assert
expressions only trigger for ICE.In the calling context some information may be present which ensures that the preconditions are always satisfied. Then, these can effectively be optimized out at compile time without changing the semantics. Similarly, the knowledge about postconditions can be integrated into the calling context.
If a function is split into an external declaration in a header and a definition in one TU, things get a bit more complicated. Here, we define an auxiliary inline definition that tests for contracts as above and calls a different internal function to access the original definition. The subtlety here is that we only want violations of preconditions in the calling context to terminate the execution, but in postconditions they should be unreachable.
Suppose a list of contracts is attached to a function declaration
without inline:
// header
returnType func(parameterTypeList)
pre(dec_pre₀ predicate_pre₀)
pre(dec_pre₁ predicate_pre₁)
…
pre(dec_preₙ predicate_preₙ)
post(dec_post₀ predicate_post₀)
post(dec_post₁ predicate_post₁)
…
post(dec_postₘ predicate_postₘ);// translation unit
returnType func(parameterTypeList) function-bodyThe effect is as-if the following function were defined in the header.
// header
inline returnType func(parameterTypeList) {
returnType _ReturnValue;
if (dec_pre₀ !predicate_pre₀) {
static_assert(ICE_OR_TRUE(predicate_pre₀), "some string");
stdc_contract_terminate("some string");
} else if (dec_pre₁ !predicate_pre₁) {
static_assert(ICE_OR_TRUE(predicate_pre₁), "some string");
stdc_contract_terminate("some string");
} else if (…) {
...
} else if (dec_preₙ !predicate_preₙ) {
static_assert(ICE_OR_TRUE(predicate_preₙ), "some string");
stdc_contract_terminate("some string");
} else {
typeof(func) func_aux;
_ReturnValue = func_aux(parameters, …);
if (dec_post₀ !predicate_post₀) {
static_assert(ICE_OR_TRUE(predicate_post₀), "some string");
unreachable();
} else if (dec_post₁ !predicate_post₁) {
static_assert(ICE_OR_TRUE(predicate_post₁), "some string");
unreachable();
} else if (…) {
...
} else if (dec_postₘ !predicate_postₘ) {
static_assert(ICE_OR_TRUE(predicate_postₘ), "some string");
unreachable();
} else {
return _ReturnValue;
}
}
stdc_contract_terminate("severe bug, execution must never reach here");
}The function func_aux will be shown
in the next section. The name func_aux
is only chosen for the example, a real implementation chooses a unique
name that is not predictable for the user.
The auxiliary inline function definition has the following properties.
ICE_OR_TRUE ensures that the static_assert
expressions only trigger for ICE.func_aux that implements the user code and
that asserts the postconditions.Note that again preconditions that are known to hold in the calling context can effectively be optimized out at compile time without changing the semantics. Similarly, postconditions can provide valuable information for the calling context. For these optimization opportunities it is essential that contracts do not have side effects.
The translation unit then provides two function symbols: the
auxiliary function func_aux that is
called by the inline version of func
as given above, and the instantiation of func itself. As mentionned, the name func_aux is only for the example; it is a
unique name that is not predictable for the user.
// translation unit
returnType func_aux(parameterTypeList) {
returnType _ReturnValue;
if (dec_pre₀ !predicate_pre₀) {
static_assert(ICE_OR_TRUE(predicate_pre₀), "some string");
unreachable();
} else if (dec_pre₁ !predicate_pre₁) {
static_assert(ICE_OR_TRUE(predicate_pre₁), "some string");
unreachable();
} else if (…) {
...
} else if (dec_preₙ !predicate_preₙ) {
static_assert(ICE_OR_TRUE(predicate_preₙ), "some string");
unreachable();
} else {
_Defer {
if (dec_post₀ !predicate_post₀) {
static_assert(ICE_OR_TRUE(predicate_post₀), "some string");
stdc_contract_terminate("some string");
} else if (dec_post₁ !predicate_post₁) {
static_assert(ICE_OR_TRUE(predicate_post₁), "some string");
stdc_contract_terminate("some string");
} else if (…) {
...
} else if (dec_postₘ !predicate_postₘ) {
static_assert(ICE_OR_TRUE(predicate_postₘ), "some string");
stdc_contract_terminate("some string");
}
}
replaced-function-body // amend return with assignment
}
}
typeof(func) func; // instantiation of funcThe only function that is called by user code is func, either as the inline definition in the
header file or via the external symbol; both types of calls have the
same semantics.
In particular, func_aux, will never
be called directly by user code.
A non-optimized version of the instantiation of func would evaluate each predicate twice,
once from the inline body of func and once from the body of funx_aux. Indeed, each predicate is first
checked as an assertion (possibly calling stdc_contract_terminate())
and then as an assumption (running into unreachable()),
so because the predicate cannot have side effects and has to evaluate
the same in both contexts.
It seems that in the advent of C++26, all major C++ compilers have implementations of C++ contracts with some of the semantics models mentioned above.
In C, there is now wide support for some form of “assume” feature,
either as builtins (clang, MSV) or as an attribute (gcc). Together with
C23’s unreachable, in C
this helps to implement the semantics with shallow inline wrappers as
described previously.
If there is no additional help from the compiler, the need to
implement two consistent wrappers (the inline version of
func and func_aux above) makes the approach a bit
tedious and not very practical as an implementation strategy.
Nevertheless, this already provides a way to test the semantics and to
see how modern compilers are able to detect redundancies in chained
contracts.
Our eĿlipsis preprocessor provides include files <ellipsis-contracts.h>,
<ellipsis-interface.h>
and <ellipsis-implementation.h>
that uses a syntax that is relatively close to the proposed syntax for
declarations with contracts, and uses them to implement the following
features:
inline
wrappers.func_aux
above)._ReturnValue in
return
statements.if statement with
declarations to implement the ghost state feature._Defer feature for a
consistent implementation of postconditions.The features _Pre, _Post and _Inv use static
strings for the diagnostics that are forcibly embedded in the
executable. These can be used to evaluate which of the contracts
actually survived optimization. Thereby a detailed assessment of the
particular optimization capabilities can be made. A small script is
provided to facilitate such an analysis.
eĿlipsis’ sources also uses contracts as described themselves. This provides a lot of test cases for the abilities of modern compilers. Evaluations with gcc and clang show interesting level of optimization, as long as not too much aliasing analysis needed for the contracts.
Thanks to Martin Uecker, David Tarditi, Joseph Myers, and Ville Voutilainen for review and discussions.