Unless otherwise stated, follow the C++ Core Guidelines. Below are exceptions to these guidelines, or more opinionated choices.
Follow the Lakos Rule (with some tweaks):
- Functions with a wide contract (i.e., with no precondition) should be marked as unconditionally
noexcept
. - Functions with a narrow contract (i.e., with preconditions), should not be marked as
noexcept
. - If a template function is conditionally-wide, (e.g., its purpose is only to be a wrapper or adapter over another function), then it may be marked conditionally
noexcept
.
In particular:
- Do not mark a function
noexcept
just because it happens not to throw. The decision should be based on the interface of the function (which includes its pre-condition), and not its implementation.
Rationale:
- Easy transition to using contracts when they come to C++.
- Enable testing for pre-condition violations by conditionally throwing.
snitch code must not directly or indirectly allocate heap (or "free store") memory while running tests. This means that a number of common C++ STL classes cannot be used (at least not with the default allocator):
std::string
: usestd::string_view
(for constant strings) orsnitch::small_string
(for variable strings) instead.std::vector
,std::map
,std::set
, and their variants: usestd::array
(for fixed size arrays) orsnitch::small_vector
(for variable size arrays) instead.std::function
: usesnitch::function_ref
instead.std::unique_ptr
,std::shared_ptr
: use values on the stack, and raw pointers for non-owning references.
Unfortunately, the standard does not generally specify if a function or class allocates heap memory or not. We can make reasonable guesses for simple cases; in particular the following are fine to use:
std::string_view
,std::array
,std::span
,std::variant
withstd::get_if
andstd::visit
,std::optional
,std::tuple
,- Functions in
<algorithm>
(exceptstd::stable_sort
,std::stable_partition
, andstd::inplace_merge
). - Concepts and type traits.
Any type or function not listed above should be assumed to use heap memory unless demonstrated otherwise. One way to monitor this on Linux is to use valgrind/massif.
Note: If necessary, it is acceptable to allocate/de-allocate heap memory when a test is not running, i.e., either at the start or end of the program, or (if single-threaded) in between test cases. For example, as of writing this, with GCC 10 on Linux and the default reporter, snitch performs heap allocations on two occasions:
- several allocations at program startup, generated by
libstdc++
initialisation. - one at the first console output, generated by
glibc
for its internal I/O buffer.
One of the advantages of snitch over competing testing framework is fast compilation of tests. To preserve this advantage, "heavy" STL headers should not be included in snitch headers unless absolutely necessary. However, they can be included in the snitch implementation *.cpp
files.
Therefore:
- Place as much code as possible in the
*.cpp
files rather than in headers. - When not possible (templates, constexpr, etc.), consider if you can use a short and clear handwritten alternative instead. For example,
std::max(a, b)
requires<algorithm>
, but can also be written asa > b ? a : b
. Some of the simplest algorithms in<algorithm>
, likestd::copy
, can also be written with an explicit loop. - Finally, consider if you really need the full feature from the STL, or just a small subset. For example, if you need a metaprogramming type list and don't need to instanciate the types, don't use
std::tuple<...>
: use a customtemplate<typename ... Args> struct type_list {}
instead.