From c0d6e85e1b5087d9f757192c64650c940c9b3c62 Mon Sep 17 00:00:00 2001 From: Dave Abrahams Date: Fri, 16 Aug 2024 13:49:18 -0700 Subject: [PATCH] Documentation (#10) Initial documentation work --- README.md | 455 +++++++++++++++++++++++++++++- include/adobe/contract_checks.hpp | 32 +-- 2 files changed, 466 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index a98a862..e00a186 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,458 @@ # Adobe Contract Checking -[![ci](https://github.com/stlab/adobe-contract-checks/actions/workflows/ci.yml/badge.svg)](https://github.com/stlab/adobe-contract-checks/actions/workflows/ci.yml) -[![codecov](https://codecov.io/gh/stlab/adobe-contract-checks/branch/main/graph/badge.svg)](https://codecov.io/gh/stlab/adobe-contract-checks) -[![CodeQL](https://github.com/stlab/adobe-contract-checks/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/stlab/adobe-contract-checks/actions/workflows/codeql-analysis.yml) +This library is for checking that software +[contracts](https://en.wikipedia.org/wiki/Design_by_contract) are +upheld. In C++ these checks can be especially important for safety +because failure to satisfy contracts typically leads to [undefined +behavior](https://en.wikipedia.org/wiki/Undefined_behavior), which can +manifest as crashes, data loss, and security vulnerabilities. This +library largely improves upon the standard +[`assert`](https://en.cppreference.com/w/cpp/error/assert) macro. + +## Design by Contract + +[Design by Contract](https://en.wikipedia.org/wiki/Design_by_contract) +is *the* industry-standard way to describe the requirements and +guarantees of any software component. It is based on three concepts: + +- **Preconditions**: what the caller of a function must ensure for the + function to behave as documented. A precondition violation + indicates a bug in the caller. + +- **Postconditions** describe a function's side-effects and return + value. Postconditions need not be upheld if the function reports an + error (such as memory exhaustion), or if preconditions were + violated. Otherwise, a postcondition violation indicates + a bug in the callee. + +- **Invariants**: conditions that always hold at some point in the + code. The most common and useful kind of invariants are **class + invariants**, which hold at any point where an instance can be + inspected from outside the class. + +A function's specification must at least describe its preconditions +and postconditions, and the specification of a class must describe its +publicly-visible invariants. Additionally describing these conditions +in code and checking them at runtime can be a powerful way to catch +bugs early and prevent their damaging effects. + +## Basic C++ Usage + +This is a header-only library. To use it from C++, simply put the +`include` directory of this repository in your `#include` path, and +`#include `. + +```c++ +#include +``` + +Every executable needs a single contract violation handler that +determines the program's behavior when a violation is detected. A +good starting point is provided by a macro that you can expand in a +source file such as the one containing your `main` function. + +```c++ +ADOBE_DEFAULT_CONTRACT_VIOLATION_HANDLER() // no semicolon +``` + +There are three primary macros used to check contracts, each with two +forms: `ADOBE_PRECONDITION`, `ADOBE_POSTCONDITION`, and +`ADOBE_INVARIANT`. Each has one required argument and one optional +argument: + +- `condition`: an expression convertible to `bool`; if `false`, the + violation handler is invoked. +- `message`: an expression convertible to `const char*` pointing to a + [null-terminated](https://en.cppreference.com/w/cpp/string/byte) + message that is additionally passed to the violation handler. The + default value is the empty string, `""`. + +For example, + +```c++ +#include +#include + +// A half-open range of integers. +// - Invariant: start() <= end(). +class int_range { + // The lower bound; if the range is non-empty, its + // least contained value. + int _start; + // The upper bound; if the range is non-empty, one + // greater than its greatest contained value. + int _end; + + void check_invariant() const { ADOBE_INVARIANT(start() <= end()); } +public: + // An instance with the given bounds. + // Precondition: end >= start + int_range(int start, int end) : _start(start), _end(end) { + ADOBE_PRECONDITION(end >= start, "invalid range bounds."); + check_invariant(); + } + + // Returns the lower bound: if *this is non-empty, its + // least contained value. + int start() const { return _start; } + + // Returns the upper bound; if *this is non-empty, one + // greater than its greatest contained value. + int end() const { return _end; } + + // Increases the upper bound by 1. + // Precondition: end() < INT_MAX + void grow_rightward() { + ADOBE_PRECONDITION(end() < INT_MAX); + int old_end = end(); + _end += 1; + ADOBE_POSTCONDITION(end() == old_end + 1); + check_invariant(); + } + + // more methods... +}; +``` + +## CMake Usage + + ```cmake + include(FetchContent) + if(PROJECT_IS_TOP_LEVEL) + FetchContent_Declare( + adobe-contract-checks + GIT_REPOSITORY https://github.com/stlab/adobe-contract-checks.git + GIT_TAG + ) + FetchContent_MakeAvailable(adobe-contract-checks) + endif() + find_package(adobe-contract-checks) + + add_library(my-library my-library.cpp) + target_link_libraries(my-library PRIVATE adobe-contract-checks) + + add_executable(my-executable my-executable.cpp) + target_link_libraries(my-executable PRIVATE adobe-contract-checks) + ``` + +### Defining a contract violation handler + +If you don't use `ADOBE_DEFAULT_CONTRACT_VIOLATION_HANDLER()` to +inject a definition, you'll need to define this function +with external linkage: + +```c++ + [[noreturn]] void ::adobe::contract_violated( + const char *const condition, + ::adobe::contract_violation::kind_t kind, + const char *const file, + std::uint32_t const line, + const char *const message) noexcept; +``` + +The parameters are as follows: + +- `condition`: a [null-terminated byte + string](https://en.cppreference.com/w/cpp/string/byte) string + containing the text of the checked condition, or `""` if + [`ADOBE_NO_CONTRACT_CONDITION_STRINGS`](#symbols-that-minimize-generated-code-and-data) + is set. +- `kind`: `precondition`, `postcondition`, or `invariant`. +- `file`: a [null-terminated byte + string](https://en.cppreference.com/w/cpp/string/byte) string + containing the name of the source file as it was passed to the + compiler, or `""` if + [`ADOBE_NO_CONTRACT_FILENAME_STRINGS`](#symbols-that-minimize-generated-code-and-datat) + is set. +- `line`: the line number of on which the failing check was written. +- `message`: the second argument to the failing check macro, or "" if + none was passed. + +If, against our advice (see [recommendation 1](#recommendations)) you +decide not to terminate the program in response to a contract +violation, you will omit `[[noreturn]]` and/or `noexcept`, and define +the corresponding [preprocessor +symbols](#contract-handler-definition). + +In your release builds you may wish to use a more minimal inline +violation handler, in which case you'll define +[`ADOBE_CONTRACT_VIOLATED_INLINE_BODY`](#contract-handler-definition). + +## Recommendations + +1. Use a contract violation handler (`adobe::contract_violated`) that + unconditionally terminates the program (rationale: see [About + Defensive Programming](#about-defensive-programming)). The + predefined contract violation handlers provided by this library + follow this recommendation. + +2. Start by checking whatever you can, and worry about performance + later. Checks are often critical for safety. [Configuration + options](#configuration) can be used to mitigate or eliminate costs + later if necessary. + +3. If you have to prioritize, precondition checks are the most + important; they are your last line of defense against undefined + behavior. Postcondition checks overlap somewhat with what will be + checked by unit tests. They still provide value because unit tests + don't cover all possible inputs and the checks may fire outside of + testing. + + Class invariant checks can give you the most bang for your buck + because they can be used to eliminate the need for precondition + checks and verbose documentation across many functions. + + ```c++ + // Returns the day of the week corresponding to the date described + // by "--" (interpreted in ISO standard date + // format) + day_of_the_week day(int year, int month, int day) { + ADOBE_PRECONDITION(is_valid_date(year, month, day)); + // implementation starts here. + } + + // Returns the day of the week corresponding to `d`. + day_of_the_week day(date d) { + // implementation starts here. + } + ``` + + The second function above benefits by accepting a `date` type whose + invariant ensures its validity. + +4. The conditions in your checks should not have side-effects that + change program behavior, because checks are sometimes turned off by + configuration. + +2. Group all precondition checks immediately after a function's + opening brace, and don't allow any code to sneak in before them. + +3. Group all postcondition checks just before your function returns. + (That may mean temporarily storing a return value in a local + variable so it can be tested.) + +4. Give your `struct` or `class` a `void check_invariant() const` + method containing `ADOBE_INVARIANT` invocations. Invoke it from each + public mutating friend or member function, just before returning, and just before passing + access to `*this` to any component outside the class. + +6. If a function throws exceptions or can otherwise report an error **to + its caller**, don't call that a precondition violation. Instead, + make that behavior part of the function's specification: document + the conditions, the resulting behavior, and test it to make sure + that it works. -## Adobe Contract Checking +2. If your program needs to take emergency shutdown measures before + termination, put those in a [terminate + handler](https://en.cppreference.com/w/cpp/error/terminate_handler) + that eventually calls + [`std::abort()`](https://en.cppreference.com/w/cpp/utility/program/abort), + and have your contract violation handler call + [`std::terminate()`](https://en.cppreference.com/w/cpp/error/terminate). + ```c++ + #include + #include + #include + #include + + [[noreturn]] void emergency_shutdown() noexcept; + + const std::terminate_handler previous_terminate_handler + = std::set_terminate(emergency_shutdown); + + [[noreturn]] void emergency_shutdown() noexcept + { + // emergency shutdown measures here. + + if (previous_terminate_handler != nullptr) + { previous_terminate_handler(); } + std::abort(); + } + + [[noreturn]] void ::adobe::contract_violated( + const char *const condition, + ::adobe::contract_violation::kind_t kind, + const char *const file, + std::uint32_t const line, + const char *const message) noexcept + { + // whatever you want here. + std::terminate(); + } + ``` + + That way, other reasons for unexpected termination, such as uncaught + exceptions, will still cause emergency shutdown. + +3. If your custom contract violation handler needs to print a + description of the failure, use [Gnu standard error + format](https://www.gnu.org/prep/standards/html_node/Errors.html#Errors), + which will be automatically understood by many tools. The + following expression will print such a report to the standard error + stream: + + ```c++ + adobe::contract_violation( + condition, kind, file, line, message).print_report(); + ``` + +5. Don't disable critical checks in shipping code unless a measurable + unacceptable performance cost is found. In that case, disable the + expensive checks selectively, e.g. + + ``` + #ifndef NDEBUG // too expensive for release + ADOBE_PRECONDITION(some_expensive_call()); + #endif + ``` + +## Rationale + +### Performance tuning and configuration complexity + +Unfortunately contract checks have some performance cost. If +programmers fear that writing a check will bake that cost into their +code, they are likely to skip writing the check altogether. To +mitigate that effect, we supply extensive +[configuration](#configuration) options that allowing projects to tune +the overheads incurred by checking, _after checks have already been +written_. + +We want programmers to write checks freely; even if for some reason +checks have to be disabled during regular development, they help to +document code and can be turned on temporarily to help track down +difficult bugs. + +### About Defensive Programming + +According to +[Wikipedia](https://en.wikipedia.org/wiki/Defensive_programming): + +> **Defensive programming** is a form of defensive design intended to +> develop programs that are capable of detecting potential security +> abnormalities and make predetermined responses.[1] It ensures the +> continuing function of a piece of software under unforeseen +> circumstances. + +In principle, defensive programming is a good idea. In practice, +though, “unforeseen circumstances” usually mean the discovery of a bug +at runtime. Trying to keep running in the presence of bugs is in +general a losing battle: + +- Code is littered with checks that obscure the logic of the program. +- The code paths that attempt to recover from bugs: + - increase program size. + - are almost never tested. + - are often incorrect. + - have unpredictable results, including data loss and security vulnerability. + - create an infinite regression of self-checking, because in theory + they could be buggy themselves. +- Bugs go undetected so code quality suffers. +- Even if bugs are logged: + - they are often deprioritized because they are not crashes, so + code quality suffers. + - developers are robbed of any chance to get a debuggable program + image; debugging becomes much harder, so code quality suffers. +- Nobody can actually think through the implications of everything + being potentially buggy, so code beomes hard to reason about. + Maintenance is more error-prone and quality suffers. + +Note that in an unsafe language like C++, a seemingly recoverable +condition like the discovery of a negative index can easily be the +result of undefined behavior that also scrambled memory or causes +“impossible” execution. + +## Development + +The usual procedures for development with cmake apply. One typical +set of commands might be: + +```sh +cmake -Wno-dev -S . -B ../build -GNinja # configure +cmake --build ../build # build/rebuild after changes +ctest --output-on-failure --test-dir ../build # test +``` + +## Reference + +### Configuration + +The behavior of this library is configured by preprocessor symbols. + +Instead of passing many `-D` options in your compiler command-line, +you can put the definition of these symbols in a header file and +simply define `ADOBE_CONTRACT_CHECKS_CONFIGURATION` as the `#include` +argument to that file: + +```sh +c++ -D \ + -I .\ + ADOBE_CONTRACT_CHECKS_CONFIGURATION="" \ + myproject_source.cpp -o myproject_executable +``` + +If you are using this project via CMake, defining the same symbol in +your `CMakeLists.txt` or on the command line will cause all clients of this +library in your build to use that configuration file. + +This library can only have one configuration in an executable, so the +privilege of configuring it belongs to the executable. In CMake, + +```cmake +if(PROJECT_IS_TOP_LEVEL) + set(ADOBE_CONTRACT_CHECKS_CONFIGURATION "") +endif() +``` + +#### Preprocessor configuration symbols + +##### Contract handler definition + +- `ADOBE_CONTRACT_VIOLATED_INLINE_BODY`: if you want the contract + violation handler to be defined inline, make this symbol expand to + its body. When the body is lighter-weight than a call to the + handler would be, an inline handler can limit the code generated at + the use site. For example, + + ```c++ + #define ADOBE_CONTRACT_VIOLATED_INLINE_BODY { ADOBE_BUILTIN_TRAP(); } + ``` + + You might use this option in release builds when a minimal handler + is required. **Note:** Defining a more complex handler inline + usually will increase binary sizes and may hurt performance. + +- `ADOBE_CONTRACT_VIOLATED_RETURNS`: define this symbol if your + contract violation handler, against our advice (see [recommendation + 1](#recommendations)), can return to its caller. + +- `ADOBE_CONTRACT_VIOLATED_THROWS`: define this symbol if your + contract violation handler, against our advice (see [recommendation + 1](#recommendations)), can throw exceptions. + +##### Symbols that minimize generated code and data + +- `ADOBE_NO_CONTRACT_CONDITION_STRINGS`: define this symbol to + suppress the generation of strings describing failed check + conditions. The empty string will be used instead. + +- `ADOBE_NO_CONTRACT_FILENAME_STRINGS`: define this symbol to suppress + the generation of strings describing the file in which failed checks + occurred. `""` will be used instead. + +- `ADOBE_SKIP_NONCRITICAL_PRECONDITION_CHECKS`: define this symbol to + make uses of `ADOBE_NONCRITICAL_PRECONDITION` generate no code. + +- `ADOBE_SKIP_ALL_CONTRACT_CHECKS`: define this symbol to make all + contract checking macros generate no code. Not recommended for + general use, but can be useful for measuring the overall performance + impact of checking in a program. + +------------------ + +[![ci](https://github.com/stlab/adobe-contract-checks/actions/workflows/ci.yml/badge.svg)](https://github.com/stlab/adobe-contract-checks/actions/workflows/ci.yml) +[![CodeQL](https://github.com/stlab/adobe-contract-checks/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/stlab/adobe-contract-checks/actions/workflows/codeql-analysis.yml) ## More Details diff --git a/include/adobe/contract_checks.hpp b/include/adobe/contract_checks.hpp index 62151f1..fb03879 100644 --- a/include/adobe/contract_checks.hpp +++ b/include/adobe/contract_checks.hpp @@ -16,8 +16,6 @@ class contract_violation final : public ::std::logic_error // The predefined kinds of contract violations provided by this library. enum predefined_kind : kind_t { precondition = 1, - // A precondition check that dynamically ensures safety failed. - safety_precondition, postcondition, invariant, unconditional_fatal_error @@ -69,10 +67,9 @@ class contract_violation final : public ::std::logic_error what()); } else { const char *const description = - _kind == predefined_kind::precondition || _kind == predefined_kind::safety_precondition - ? "Precondition violated" + _kind == predefined_kind::precondition ? "Precondition violated" : _kind == predefined_kind::postcondition ? "Postcondition not upheld" - : _kind == predefined_kind::invariant ? "Invariant violated" + : _kind == predefined_kind::invariant ? "Invariant not upheld" : "Unknown category kind"; // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg) std::fprintf(stderr, @@ -124,10 +121,10 @@ class contract_violation final : public ::std::logic_error // we cannot use it unless we have C++20. #if __cplusplus >= 2020002 && __has_cpp_attribute(unlikely) // The attribute (if any) that marks the cold path in a contract check. -#define ADOBE_CONTRACT_VIOLATION_LIKELIHOOD [[unlikely]] +#define INTERNAL_ADOBE_CONTRACT_VIOLATION_LIKELIHOOD [[unlikely]] #else // The attribute (if any) that marks the cold path in a contract check. -#define ADOBE_CONTRACT_VIOLATION_LIKELIHOOD +#define INTERNAL_ADOBE_CONTRACT_VIOLATION_LIKELIHOOD #endif // Injects a definition of ::adobe::contract_violated that reports @@ -181,24 +178,25 @@ class contract_violation final : public ::std::logic_error // to help. // Expands to its third argument -#define ADOBE_THIRD_ARGUMENT(arg0, arg1, invocation, ...) invocation +#define INTERNAL_ADOBE_THIRD_ARGUMENT(arg0, arg1, invocation, ...) invocation // ADOBE_PRECONDITION(); // ADOBE_PRECONDITION(, ); // // Expands to a statement that reports a precondition failure (and // if supplied) when is false. -#define ADOBE_PRECONDITION(...) \ - INTERNAL_ADOBE_MSVC_EXPAND(ADOBE_THIRD_ARGUMENT( \ - __VA_ARGS__, ADOBE_PRECONDITION_2, ADOBE_PRECONDITION_1, ignored)(__VA_ARGS__)) +#define ADOBE_PRECONDITION(...) \ + INTERNAL_ADOBE_MSVC_EXPAND(INTERNAL_ADOBE_THIRD_ARGUMENT( \ + __VA_ARGS__, INTERNAL_ADOBE_PRECONDITION_2, INTERNAL_ADOBE_PRECONDITION_1, ignored)( \ + __VA_ARGS__)) // Expands to a statement that reports a precondition failure when // condition is false. -#define ADOBE_PRECONDITION_1(condition) \ +#define INTERNAL_ADOBE_PRECONDITION_1(condition) \ if (condition) \ ; \ else \ - ADOBE_CONTRACT_VIOLATION_LIKELIHOOD \ + INTERNAL_ADOBE_CONTRACT_VIOLATION_LIKELIHOOD \ \ ::adobe::contract_violated(#condition, \ ::adobe::contract_violation::predefined_kind::precondition, \ @@ -208,11 +206,11 @@ class contract_violation final : public ::std::logic_error // Expands to a statement that reports a precondition failure and // when condition is false. -#define ADOBE_PRECONDITION_2(condition, message) \ +#define INTERNAL_ADOBE_PRECONDITION_2(condition, message) \ if (condition) \ ; \ else \ - ADOBE_CONTRACT_VIOLATION_LIKELIHOOD \ + INTERNAL_ADOBE_CONTRACT_VIOLATION_LIKELIHOOD \ \ ::adobe::contract_violated(#condition, \ ::adobe::contract_violation::predefined_kind::precondition, \ @@ -220,7 +218,7 @@ class contract_violation final : public ::std::logic_error __LINE__, \ message) -#define ADOBE_POSTCONDITION(condition) -#define ADOBE_INVARIANT(condition) +#define ADOBE_POSTCONDITION(...) ADOBE_PRECONDITION(__VA_ARGS__) +#define ADOBE_INVARIANT(...) ADOBE_INVARIANT(__VA_ARGS__) #endif