Professional Documents
Culture Documents
If you work in high-integrity or safety-critical software then you have a duty-of-care to:
Ensure your code does what it should
Ensure your code doesn’t do what it’s not supposed to.
Moreover, you’re typically required to both demonstrate and document how you’ve achieved this duty-of-care. Typical
mechanisms for demonstrating compliance include unit testing and static analysis; documentation usually includes function
specifications and test results. The amount of testing, static analysis and documentation you (have to) do depends on the
integrity level of your system. The more critical the consequences of a failure, the more evidence you have to provide to
demonstrate that you’ve considered, and alleviated, the failure.
I would argue that, even if your code could never kill someone, its failure will still ruin someone’s day. As a professional
engineer you always have some duty-of-care to ensure your system works ‘properly’. Therefore, you always should do an
‘appropriate’ level of testing, analysis and documentation.
Writing dependable code is a combination of two things:
Ensuring your code is implemented correctly (by you)
Ensuring your code is used correctly (by others)
In my experience, engineers are normally pretty good at implementing functionality correctly. Where they are usually
weaker is in taking the time and effort to specify exactly what they should be implementing. In particular, error paths,
corner cases and combination of these things tend to get overlooked. That is, less experienced engineers tend to focus on
the ‘sunny day’ scenario, and be overly optimistic of the other’s code around them.
Ensuring your code is used correctly is normally an exercise in documentation. However, documentation suffers from a
number of challenges:
We’re not given the time to do it well
No-one likes writing it.
Good documentation is a rarity; probably because of the above
No-one reads documentation anyway; also probably because of the above.
Your code can’t be automatically checked against the documentation
The above means that documentation is often an exercise in futility for the writer, and an exercise in frustration for the
reader.
So, in this article I want to explore language features, existing and forthcoming, that can help us improve the quality of our
code. For the purposes of this article I’ll concentrate on one aspect of C++ – functions.
In order to be more effective than simple text documentation these features must provide the following characteristics:
They should guide the implementer as to corner cases, error paths, etc.
They should establish a (bare minimum) set of unit test cases
They should specify any requirements imposed on the user
They should establish guarantees – promises – provided by the code to the client
They should be automatically checkable – ideally at compile-time
They should make code more readable
They should, wherever possible, improve a compiler’s ability to optimise code.
UPDATE:
Since writing this article Contracts have been dropped from C++20. The code presented here remains incomplete and
experimental. Here’s hoping this very important feature makes it into a release sometime soon.
Contracts
The idea of design-by-contract is not new. It was first coined by Bertrand Meyer in 1986 and implemented in his Eiffel
programming language. Design-by-contract has its roots in formal specification techniques.
Design-by-contract is based around a metaphor that software modules cooperate based around the idea of a contract. The
contract specifies:
A set of obligations the client must fulfil, known as the preconditions. The server benefits by not having to handle cases
outside of the precondition.
A set of guarantees the server will deliver – the postcondition. The server is obliged to guarantee this. Obviously, the
client benefits by having guaranteed properties after the call
Certain states that will be maintained – known as the invariant.
The principle of design-by-contract is that many programming errors can be caught (or even avoided) by specifying
contractual obligations for the caller (pre-conditions) and guarantees/promises required of the function itself (post-
conditions).
Given enough information (strong typing, variable permissible ranges, etc.) contracts should be checkable at compile-time.
Languages such as Ada (and to an extent, Rust) provide such capabilities. For C++, function pre- and post-condition
checking depends on the actual values of program objects contract checking has to be a run-time facility. This, of course,
has run-time performance impact, so it should be possible to disable contract checking for a particular system build (for
example, you will probably want contract checking enabled in debug builds, but disabled for release builds)
In C++20 contract specification was formalized using the attributes mechanism of the language.
Assertion
C programmers are no doubt familiar with the assert macro. This has been replaced in C++20 with the assertion attribute.
The assertion attribute takes an expression in the form of a predicate. If the predicate evaluates to true execution
continues. If the predicate evaluates to false a contract violation occurs (which we discuss below)
void func(int a, int b)
{
// Some code...
This works just like the C assert macro. However, this isn’t a function-like macro, as C’s assert is, so it doesn’t suffer from
problems of function-like macro expansion. For example, the following won’t compile.
struct Coordinate {
int x;
int y;
};
However, using the assert attribute removes the error
void func(const Coordinate& pos)
{
[[ assert: pos == Coordinate { 0, 0 } ]]; // OK
}
The predicate must not have any side-effects; that is, they must not change the state of any objects in the system – either
local variables or statics. A predicate with a side-effect is undefined behaviour (and, given that we’re trying to reduce the
opportunity for programming errors, must be considered A Bad Thing).
A predicate can also include function calls
bool is_valid(int x);
Preconditions
Preconditions are specified with the ‘expects’ attribute. The attributes are applied to the function’s declaration, not its
definition.
Precondition predicates are evaluated prior to entering the function’s body. If a predicate evaluates to false this is a
contract violation.
void func(int a) [[ expects: a < 100 ]];
You can have multiple preconditions. They are evaluated in order of declaration.
Error queue_create(
QUEUE * const queue_handle,
std::size_t elem_size,
std::size_t queue_size
)
[[ expects: queue_handle != nullptr ]]
[[ expects: elem_size > 0 ]]
[[ expects: queue_size > 0 ]];
As with assertions, a precondition predicate can include a function call; but cannot have a side-effect
class Buffer {
public:
void add(int data);
int get()
bool is_empty() const;
};
Preconditions are, wherever possible, evaluated at the call site. This is what you want the majority of the time.
Establishing preconditions on a function, as well as reduce the opportunity for programming errors, also gives the compiler
the potential to optimise code, given that there are now constraints on the parameters that will not be violated.
Postconditions
Postconditions are specified with the ‘ensures’ attribute. Postconditions are evaluated at the exit of the function.
void allocate_ADT(ADT* ptr)
[[ expects: ptr == nullptr ]]
[[ ensures: ptr != nullptr ]]
Postcondition predicates can use the return value from the function. Since the return value object doesn’t have a name you
can (for the lifetime of the predicate) give it a name
int count_items(const vector<int>& v)
[[ ensures result : result >= 0 ]];
As previously, postcondition predicates can call functions, and cannot have side effects. Additionally, the postcondition
predicate cannot modify the return value object.
bool is_valid(int val);
int func()
[[ ensures result : is_valid(result) ]];
(Yes the function being called in the postcondition can itself have preconditions. I’ll leave it as the perennial ‘exercise for the
reader’ for you to work out what should happen)
Finally, if (for some deranged reason) you are use automatically-deduced function return types you cannot put
postconditions on the function declaration
auto make_something()
[[ ensures result : true ]] ; // Error - what's the type
// of result?!
Contract violation
Violating a contract causes the compiler to generate a std::contract_violation object, as follows
namespace std {
Although the details of the contract_violation are implementation-defined, the standard encourages certain responses:
In the case of a precondition violation implementations are encouraged to report the caller site source location.
In the case of a postcondition violation the source location violation will be the one from the callee (server) site.
In the case of an assertion, the source location will be the one from the assertion itself.
The comment should contain text relating to the conditional-expression of the contract that was not satisfied.
The constraint_violation object is passed to a violation handler function, of the form
void (*)(const std::constraint_violation&)
(Please note: This output is pure speculation. At the time of writing there are no implementations of contracts)
The API should also allow you to write your own violation handler; to do whatever you may want to for your system.
Contract levels
Evaluating contract constraints has the potential to be very expensive; and we don’t always need to perform run-time
checks of all constraints. To aid in this each constraint can be given a contract level; one of the following:
default
This is (as the name suggests) the default value. The default contract level is suggested for those contract predicates
that are cheap (or rather, not expensive) to perform; and so can be performed without significantly affecting the
performance of the system.
audit
This level is recommended for constraints that are expensive to perform and should therefore be limited to
development-type builds.
axiom
Constraints marked as axiom are not included in run-time checks. However, it is envisaged that axiom constraints could
be subjected to static analysis.
The contract level is specified as an optional parameter on each constraint
double square(double x) [[ expects axiom : x >= 0 ]];
Which constrain level is currently active is selected via the build level. How the build level is set is implementation-defined
(but again, I’m presuming a compiler flag). The build level has one of three values:
off. No contract violation checking is performed
default. Contract checking is only performed on those constraints specified as default.
audit. Contract checking is performed on all constraints specified as either default or audit.
Function attributes
while we’re talking about attributes and extending function specifications, I should probably mention some of the other
attributes that can be applied to functions. Some of these have been around since C++11, some were introduced in C++17
[[ maybe_unused ]] (from C++17)
This instructs the compiler to suppress any warnings about objects (parameters) that may not be used in the function.
Normally you shouldn’t have unused parameters in functions (if you’re not going to use them, don’t add them!). This
attribute may be useful with interfaces/virtual functions: an interface should represent an abstract service; a particular
implementation may not require all parameters (whereas others might).
[[ nodiscard ]] (from C++17)
Emits a diagnostic if the return value from the function is not used (read). For example, if a function returns an error
code it is probably good practice to ensure the caller checks that error.
[[ noreturn ]] (from C++11)
Indicates that this function never exits (that is, an infinite loop); will emit a diagnostic if the function has a (reachable)
return statement. This attribute is useful for indicating thread functions for run-forever (‘static’) tasks in concurrent
systems, for example.
[[ deprecated ]] (from C++14)
Issues a diagnostic warning that the function should not be used; and is superseded by another.
void uses_some(
int param1,
int param2 [[ maybe_unused ]],
int param3 [[ maybe_unused ]]
);
Summary
In my view C++20’s contracts (along with concepts) are one of the most important new features in C++. They change the
way we specify and write C++ code.
One of the key points that comes out of this, though, is the fundamental requirement to design a system before coding.
This might sound blindingly obvious, but in my experience many engineers prefer to code their way out of problems, rather
than design them out before they ever become a problem.
By adding these new mechanisms, hopefully we’ll see a step-change in the quality of C++ programs being constructed.
Glennan Carnie
Technical Consultant at Feabhas Ltd
Glennan is an embedded systems and software engineer with over 20 years experience, mostly in high-integrity systems for the defence and aerospace
industry.
He specialises in C++, UML, software modelling, Systems Engineering and process development.
Like (2) Dislike (0)
This entry was posted in C/C++ Programming and tagged assert, attributes, C++11, C++14, C++17, C++20, C++2a, Contracts, deprecated, ensures, expects, maybe_unused, Modern C++, nodiscard,
noreturn. Bookmark the permalink.
Contracts have been removed from c++20, so they won't be available before c++23, and probably in a different form. Your article is
misleading on this matter.
Like (2) Dislike (0)
Dan says:
September 19, 2019 at 5:14 pm
Nice article, I've been using assert (compile and run time) in my embedded code, I generally don't use exceptions (embedded code),
perhaps this is a step forward.
(small typo) -- "Until C++20 we had two options for handling this type of program error:" -- and then 3 options are listed. Of course there
are programming jokes about counting and off-by-one errors, perhaps that was the intent, but it doesn't read as such.
Like (2) Dislike (0)