Skip to content

Commit

Permalink
check macros -> assertion macros
Browse files Browse the repository at this point in the history
  • Loading branch information
nessan committed Jul 27, 2024
1 parent 2c238a6 commit d55985f
Show file tree
Hide file tree
Showing 26 changed files with 317 additions and 297 deletions.
6 changes: 3 additions & 3 deletions docs/_quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,11 @@ website:
- section: "Debugging"
contents:
- text: "Assertions"
href: content/bit_assert/index.qmd
href: content/bit_assertion/index.qmd
- text: "Compiler Flags"
href: content/bit_assert/index.qmd#compiler-flags
href: content/bit_assertion/index.qmd#compiler-flags
- text: "Design Rationale"
href: content/bit_assert/index.qmd#design-rationale
href: content/bit_assertion/index.qmd#design-rationale
- text: "---"
- section: "Technical Notes"
contents:
Expand Down
9 changes: 5 additions & 4 deletions docs/content/_common.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ $$
$$
:::

<!-- Our check/assert macros and compiler flags that control them -->
[`BIT_DEBUG`]: /content/bit_assert/index.qmd
[`bit_debug_assert`]: /content/bit_assert/index.qmd
[`bit_always_assert`]: /content/bit_assert/index.qmd
<!-- Our assertion macros and compiler flags that control them -->
[`BIT_DEBUG`]: /content/bit_assertion/index.qmd
[`NDEBUG`]: /content/bit_assertion/index.qmd
[`bit_assertion`]: /content/bit_assertion/index.qmd
[`bit_debug_assertion`]: /content/bit_assertion/index.qmd

<!-- Formatted links to the pages for the bit::vector class -->
[`bit::vector`]: /content/vector/index.qmd
Expand Down
136 changes: 0 additions & 136 deletions docs/content/bit_assert/index.qmd

This file was deleted.

146 changes: 146 additions & 0 deletions docs/content/bit_assertion/index.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
---
title: Assertions
---

{{< include /content/_common.qmd >}}

## Introduction

The `bit_assertion.h` header has two replacements for the standard [`assert`] macro --- they allow for an additional string output that you can use to print the values of the variables that triggered any failure.

```cpp
bit_assertion(condition, message) // <1>
bit_debug_assertion(condition, message) // <2>

```
1. This is very similar to the standard [`assert`] macro --- the assertion is always checked **unless** the `NDEBUG` flag is set at compile time.
2. On the other hand, this type of assertion is **only** checked if the `DEBUG` flag is set at compile time.
Assuming a check is "on", in all cases, if `condition` evaluates to `false`, these both call the macro:
```cpp
bit_exit(message)
```
The `bit_exit(message)` macro passes the `message` and the source code location of the failure to `bit::exit(...)`.
That function prints the source code location of the failure along with the `message` payload and then **exits** the program.

The `bit::exit` function needs source code location parameters (the filename, the line number, and the function name), and the `bit_exit` macro automatically adds those.
You typically use the `message` argument to print the values of the variables that triggered the failure.
The message can be anything that can be formatted using the facilities in [`std::format`].

::: {.callout-tip}
# Microsoft compiler
Microsoft's old traditional preprocessor is not happy with these macros, but their newer cross-platform compatible one is fine.
Add the `/Zc:preprocessor` flag to use that upgrade at compile time.
Our `CMake` module `compiler_init` does that automatically for you.
:::

## Compiler Flags

Flag | Description
------------- | -----------
`BIT_DEBUG` | If you set the `BIT_DEBUG` flag, the library will perform demanding but potentially useful safety assertions on indices, size equality checks, etc. Otherwise, all the `bit_debug_assertion` calls are no-ops.
`NDEBUG` | This works exactly like it does for the standard [`assert`] macro. If it is set then all `bit_assertion` calls are np-ops.
: {.bordered .striped .hover .responsive tbl-colwidths="[20,80]"}

::: {.callout-note}
# Compiler Flag Consistency
If you set the `NDEBUG` flag, we make sure that the `BIT_DEBUG` flag is not set.
:::

## Examples

[Example --- Snippet from the `bit::vector<>::set` method]{.bt}
```cpp
/// @brief Set the element at index `i` to 1.
constexpr bit::vector &set(std::size_t i)
{
bit_debug_assertion(i < m_size, "index `i` = " << i << " must be < `m_size` which is " << m_size);
...
}
```
Here `m_size` is holds the size of the vector --- so we must have `i < m_size`
To check every element access, set the `BIT_DEBUG` flag during compiles.
If the assertion fails, the program exits with an error message that gives the offending values.
The `bit_debug_assertion` line expands to *nothing* if the `BIT_DEBUG` flag is not set during compiles.
[Example --- Message from an assertion failure]{.bt}
```cpp
#undef NDEBUG // <1>
#define BIT_DEBUG
#include <bit/bit.h>
int main()
{
std::size_t n = 12; // <2>
bit::vector<> v(n);
v.set(n); // <3>
std::cout << v << "\n";
}
```
1. For the sake of the example we added code lines to make sure the `bit_assertion` is triggered. In normal usage, these flags are passed through the compiler command line.
2. Construct a vector of size 12 and then attempt to set the "last" element.
3. A deliberate but typical *off-by-one* index error as the valid indices are from 0 to `n-1`, which is 11.

[Output]{.bt}
```sh
BIT ASSERTION FAILURE:
Function 'set' (vector.h, line 910):
Statement 'i < m_size' is NOT true: Index i = 12 must be < `m_size` = 12
```
The program will then exit.

## Design Rationale

### `bit_debug_assertion`

In the development cycle, it can be helpful to range-check indices and so on.
However, those checks are expensive and can slow down numerical code by orders of magnitude.
Therefore, we don't want there to be any chance that those verifications are accidentally left "on" in the production code.
The `bit_debug_assertion(...)` form covers this type of verification.
Turning on these checks requires the programmer to take a specific action --- namely, she must set the `BIT_DEBUG` flag during compile time.

For example, here is a pre-condition from a hypothetical `dot(Vector u, Vector v)` function:
```cpp
bit_debug_assertion(u.size() == v.size(), "Vector sizes {} and {} DO NOT match!", u.size(), v.size());
```
This code checks that the two vector arguments have equal length --- a necessary constraint for the dot product operation to make sense.
If the requirement is not satisfied, the code will exit with an informative message that includes the size of the two vectors.
The check here is **off** by default, and you need to do something special (i.e., define the `BIT_DEBUG` flag at compile time) to enable it.
The idea is that production code may do many of these dot products, and we do not generally want to pay for the check.
However, enabling these sorts of checks may be very useful during development.
The `bit_debug_assertion(...)` macro expands to nothing **unless** you set the `BIT_DEBUG` flag at compile time.
### `bit_assertion`
On the other hand, there are some checks where the cost of the assertion is slight when compared to the work of the function.
Leaving these checks on in production is not likely to impose much of a performance penalty.
For example, a pre-condition for a matrix inversion method is that the input matrix must be square.
Here is how we do that check in an `invert(const Matrix& M)` function:
```cpp
bit_assertion(M.is_square(), "Cannot invert a {} x {} NON-square matrix!", M.rows(), M.cols());
```
We can only invert square matrices.
The `M.is_square()` call checks that condition and, on failure, exits the program with a helpful message.
The check cost is very slight compared to the work done by the `invert(...)` method, so leaving it on even in production code is not a problem.

The check here is **on** by default, and you need to do something special (i.e., define the `NDEBUG` flag at compile time) to disable it.

The `bit_assertion(...)` macro expands to nothing **only if** you set the `NDEBUG` flag at compile time.

The decision to use one form vs. the other depends on the cost of doing the check versus the work done by the method in question.
A primary use case for `bit_debug_assertion` is to do things like bounds checking on indices --- from experience, this is vital during development.
However, bounds-checking every index operation incurs a considerable performance penalty and can slow down numerical code by orders of magnitude.
So it makes sense to have the checks in place for development but to ensure they are never there in release builds.

::: {.callout-note}
# Macro-land
We are in macro land here, so there are no namespaces.
Typically, macros have names in caps, but the standard `assert` does not follow that custom, so neither do these `bit_assertion` macros.
:::

### See Also
[`assert`]
2 changes: 1 addition & 1 deletion docs/content/matrix/access.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,4 @@ Col 2: [1 0 1 0]

### See Also
[`vector::reference`] \
[`bit_assert`]
[`bit_assertion`]
4 changes: 2 additions & 2 deletions docs/content/matrix/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,8 @@ Method | Description
Macro | Description
----- | -----------
[`BIT_DEBUG`] | This compile-time flag enables extra safety checks.
[`bit_debug_assert`] | These assertions are only checked if the `BIT_DEBUG` flag is set at compile time.
[`bit_always_assert`] | Use this form for checks that must always be performed.
[`bit_debug_assertion`] | These assertions are only checked if the `BIT_DEBUG` flag is set at compile time.
[`bit_assertion`] | Use this form for checks that must always be performed.
: {.bordered .striped .hover .responsive tbl-colwidths="[35,65]"}

## Non-member Functions
Expand Down
2 changes: 1 addition & 1 deletion docs/content/matrix/invert.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Contrast that to matrices over the reals where, mathematically at least, matrice
::: {.callout-warning}
# The bit-matrix argument must be square
The input matrix must be square, and the [`bit_always_assert`] macro checks that pre-condition.
The input matrix must be square, and the [`bit_assertion`] macro checks that pre-condition.
:::
[Example]{.bt}
Expand Down
2 changes: 1 addition & 1 deletion docs/content/matrix/pow.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ It is also worth noting that all arithmetic in $\FF$ is mod 2, so there are no o
::: {.callout-warning}
# The bit-matrix argument must be square
The input matrix must be square, and the [`bit_always_assert`] macro checks that pre-condition.
The input matrix must be square, and the [`bit_assertion`] macro checks that pre-condition.
:::
[Example]{.bt}
Expand Down
2 changes: 1 addition & 1 deletion docs/content/matrix/swap.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ These methods return a reference to `*this`, so can be chained with other calls.
Generally, these methods do *not* check whether the indices are in bounds.
If they aren't, the behavior is undefined (but bound to be wrong!)
All of them will perform range checking if you set the `BIT_DEBUG` at compile time.
See [`bit_assert`].
See [`bit_assertion`].
:::
[Example]{.bt}
Expand Down
5 changes: 3 additions & 2 deletions docs/content/notes/design.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,10 @@ For this reason, we include our versions of the standard [`std::assert`] macro.
Our assertions come with the ability to print an explanation of what caused any failure.
:::

The most commonly used form in the library is `bit_debug_assert(...)`.
The most commonly used form in the library is `bit_debug_assertion(...)`.
This form expands to nothing *unless* the programmer sets the `BIT_DEBUG` flag at compile time.
That is typically done automatically *only* for debug software builds and is *never* done for release/optimized builds.

There is also a version `bit_always_assert(...)` for checks that should always be carried out no matter what flags are passed to the compiler.
There is also a version `bit_assertion(...)` that is very similar to the [`std::assert`] macro with the addition of a message.
Typically these are for assertions where the cost of the check is cheap compared to the cost of the work done in the method.
This form expands to nothing *only if* the `NDEBUG` flag is set a compile time.
2 changes: 1 addition & 1 deletion docs/content/polynomial/access.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,4 @@ After call v = []
### See Also
[`polynomial::reference`] \
[`polynomial::size`] \
[`bit_assert`]
[`bit_assertion`]
2 changes: 1 addition & 1 deletion docs/content/polynomial/evaluation.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ The sum uses [Horner's method](https://en.wikipedia.org/wiki/Horner%27s_method).

::: {.callout-warning}
# The bit-matrix argument must be square
The input matrix must be square, and the [`bit_always_assert`] macro checks that pre-condition.
The input matrix must be square, and the [`bit_assertion`] macro checks that pre-condition.
:::

[Example]{.bt}
Expand Down
Loading

0 comments on commit d55985f

Please sign in to comment.