John McFarlane
Software Engineer, Ennis, Co Clare
github.com/johnmcfarlane/accu-2022-examples
johnmcfarlane.github.io/cpp-belfast-2022
Work: games, servers, automotive
Fun: numerics, workflow, word games
C++: low latency, numerics, contracts
Contract Programming in C++(20)
Alisdair Meredith, CppCon 2018
A contract is an exchange of promises between a client and a provider.
P0157R0: Handling Disappointment in C++
Lawrence Crowl, 2015
When a function fails to do what we want, we are disappointed. How do we report that disappointment to callers? How do we handle that disappointment in the caller?
P0709R2: Zero-overhead deterministic exceptions: Throwing values
Herb Sutter, 2018
Programming bugs (e.g., out-of-bounds access, null dereference) and abstract machine corruption (e.g., stack overflow) cause a corrupted state that cannot be recovered from programmatically, and so they should never be reported to the calling code as errors that code could somehow handle.
C++ API | standard | end user | test user | |
---|---|---|---|---|
agreement | docs | ISO/IEC 14882 | docs | docs |
client | dev | dev | user | dev |
provider | dev | implementer | dev | implementer |
violation | bug | bug | error | error |
C++ API | standard | end user | test user | |
---|---|---|---|---|
agreement | docs | ISO/IEC 14882 | docs | docs |
client | dev | dev | user | dev |
provider | dev | implementer | dev | implementer |
violation | bug | bug | error | error |
C++ API | standard | end user | test user | |
---|---|---|---|---|
agreement | docs | ISO/IEC 14882 | docs | docs |
client | dev | dev | user | dev |
provider | dev | implementer | dev | implementer |
violation | bug | bug | error | error |
C++ API | standard | end user | test user | |
---|---|---|---|---|
agreement | docs | ISO/IEC 14882 | docs | docs |
client | dev | dev | user | dev |
provider | dev | implementer | dev | implementer |
violation | bug | bug | error | error |
C++ API | standard | end user | test user | |
---|---|---|---|---|
agreement | docs | ISO/IEC 14882 | docs | docs |
client | dev | dev | user | dev |
provider | dev | implementer | dev | implementer |
violation | bug | bug | error | error |
C++ API | standard | end user | test user | |
---|---|---|---|---|
agreement | docs | ISO/IEC 14882 | docs | docs |
client | dev | dev | user | dev |
provider | dev | implementer | dev | implementer |
violation | bug | bug | error | error |
std::filesystem
and
std::string
are UI elements!
std::chrono
models the real world and similarly 'messy'.
How does your program handle errors?
It depends.
C++ has too many error-handling facilities.
But part of the problem is its versatility.
An important consideration is to allow for versatility.
namespace acme {
// everything needed for program to do its thing;
// well-formed and error-free
struct sanitized_input {
// ...
};
// safety boundary; untrusted input; trusted output
std::optional<sanitized_input> digest_input(std::span<char const* const> args);
// free from error handling
std::string do_the_thing(sanitized_input in);
}
int main(int argc, char const* const* argv)
{
// variable binding; type safety FTW!
auto const args{std::span{argv, argv+argc}};
auto const input{acme::digest_input(args)};
if (!input) {
return EXIT_FAILURE;
}
std::cout << acme::do_the_thing(*input);
return EXIT_SUCCESS;
}
// print file's size or return false
auto print_file_size(char const* filename)
{
std::ifstream in(filename, std::ios::binary | std::ios::ate);
if (!in) {
std::cerr << std::format("failed to open file \"{}\"\n", filename);
return false;
}
std::cout << std::format("{}\n", in.tellg());
return true;
}
auto print_config_file_size()
{
if (!print_file_size("default.cfg")) {
// in this function, we know the nature of the file
std::cerr << "failed to print the size of the config file\n";
}
}
// return file's size
auto file_size(char const* filename)
{
std::ifstream in(filename, std::ios::binary | std::ios::ate);
if (!in) {
std::cerr << std::format("failed to open file \"{}\"\n", filename);
// how is the disappointment returned now?
}
return in.tellg();
}
auto file_size(char const* filename)
-> std::optional<std::ifstream::pos_type>
{
std::ifstream in(filename, std::ios::binary | std::ios::ate);
if (!in) {
std::cerr << std::format("failed to open file \"{}\"\n", filename);
return std::nullopt;
}
return in.tellg();
}
// error handler function
template <typename... args>
[[noreturn]] void fatal(args&&... parameters)
{
std::cerr << std::format(std::forward<args>(parameters)...);
std::abort();
}
int main(int argc, char* argv[])
{
auto const expected_num_params{3};
if (argc != expected_num_params) {
fatal(
"Wrong number of arguments provided. Expected={}; Actual={}\n",
expected_num_params, argc);
return EXIT_FAILURE;
}
}
There are zero or more obstacles and one finish line.
auto do_something(auto param)
{
// hurdle 1
auto intermediate_thing1 = get_a_thing(param)
if (!intermediate_thing1) {
return failure;
}
// hurdle 2
auto intermediate_thing2 = get_another_thing(intermediate_thing1)
if (!intermediate_thing2) {
return failure;
}
// finish line
return intermediate_thing2;
}
C++ API | standard | end user | test user | |
---|---|---|---|---|
agreement | docs | ISO/IEC 14882 | docs | docs |
client | dev | dev | user | dev |
provider | dev | implementer | dev | implementer |
violation | bug | bug | error | error |
C++ API | standard | end user | test user | |
---|---|---|---|---|
agreement | docs | ISO/IEC 14882 | docs | docs |
client | dev | dev | user | dev |
provider | dev | implementer | dev | implementer |
violation | bug | bug | error | error |
A program with a bug:
en.wikipedia.org/wiki/PID_controller#Mathematical_form
en.wikipedia.org/wiki/PID_controller#Mathematical_form
namespace pid {
struct components {
double proportional;
double integral;
double derivative;
};
// values kept constant throughout operation of a controller
struct parameters {
// non-negative factors used to generate PID terms
components k;
double dt;
};
struct state {
double integral;
double error;
};
struct input {
// desired value
double setpoint;
// actual value
double process_variable;
};
struct result {
// corrective value to apply to system
double correction;
// to pass in to next iteration as input::previous state
state current;
};
[[nodiscard]] auto calculate(parameters params, state previous, input in)
-> result;
}
#include "pid.h"
#include "pid_assert.h"
[[nodiscard]] auto pid::calculate(parameters params, state previous, input in)
-> result
{
PID_ASSERT(params.k.proportional >= 0);
PID_ASSERT(params.k.integral >= 0);
PID_ASSERT(params.k.derivative >= 0);
PID_ASSERT(params.dt > 0);
auto const error = in.setpoint - in.process_variable;
auto const next_integral{previous.integral + error * params.dt};
auto const derivative = (error - previous.error) / params.dt;
auto const terms{components{
.proportional = params.k.proportional * error,
.integral = params.k.integral * next_integral,
.derivative = params.k.derivative * derivative}};
auto const output = terms.proportional + terms.integral + terms.derivative;
return result{
output,
state{next_integral, error}};
}
typedef uid = std::uint32_t;
constexpr auto invalid_id{uid{-1}};
...
class bitset {
public:
bool get(std::size_t index) const {
if (index >= size()) {
resize(index);
}
...
}
...
};
invalid_id
, are trouble!C++ API | standard | end user | test user | |
---|---|---|---|---|
agreement | docs | ISO/IEC 14882 | docs | docs |
client | dev | dev | user | dev |
provider | dev | implementer | dev | implementer |
violation | bug | bug | error | error |
C++ API | standard | end user | test user | |
---|---|---|---|---|
agreement | docs | ISO/IEC 14882 | docs | docs |
client | dev | dev | user | dev |
provider | dev | implementer | dev | implementer |
violation | bug | bug | error | error |
Prominent C++ Standard contract violation bugs fall into two main categories
int main()
{
return 1/0;
}
int main()
{
auto v{std::vector{0, 1}};
v.push_back(2);
fmt::print("{}\n", v[3]);
}
C++ API | standard | end user | test user | |
---|---|---|---|---|
agreement | docs | ISO/IEC 14882 | docs | docs |
client | dev | dev | user | dev |
provider | dev | implementer | dev | implementer |
violation | bug | bug | error | error |
C++ API | standard | end user | test user | |
---|---|---|---|---|
agreement | docs | ISO/IEC 14882 | docs | docs |
client | dev | dev | user | dev |
provider | dev | implementer | dev | implementer |
violation | bug | bug | error | error |
assert
.
// For testing coverage, assertions are not necessarily a concern.
#if defined(PID_DISABLE_ASSERTS)
#define PID_ASSERT(cond)
// In debug builds, fail fast and loud when an assertion is challenged.
#elif !defined(NDEBUG)
#define PID_ASSERT(cond) ((cond) ? static_cast<void>(0) : std::terminate())
// In optimised GCC builds, optimise/sanitize accordingly.
#elif defined(__GNUC__)
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define PID_ASSERT(cond) ((cond) ? static_cast<void>(0) : __builtin_unreachable())
// In optimised MSVC builds, optimise/sanitize accordingly.
#elif defined(_MSC_VER)
#define PID_ASSERT(cond) __assume(cond)
// In other optimised builds assume code is correct.
#else
#define PID_ASSERT(cond)
#endif
// precondition: number is in range [1..26]
constexpr auto number_to_letter(int number)
{
return char(number - 1 + 'A');
}
// signed integer overflow violates C++ Standard, is already UB
number_to_letter(0x7fffffff);
constexpr auto number_to_letter(int number)
{
constexpr auto lookup_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
return lookup_table[number - 1];
}
// signed integer overflow violates C++ Standard, is already UB
number_to_letter(0x7fffffff);
A failing assertion is now tied in with the fault-protection system and by default places the spacecraft into a predefined safe state where the cause of the failure can be diagnosed carefully before normal operation is resumed.
"Doesn't look like anything to me"
John McFarlane
github.com/johnmcfarlane/accu-2022-examples
johnmcfarlane.github.io/cpp-belfast-2022
int f(int const* p, int a, int b)
{
// Are we good?
int r = 0;
for (int i = a; i <= b; i ++)
{
r += p[i];
}
return r;
}
int g(int const* p)
{
// Is this OK?
return f(p, -1, 1);
}
maybe a bug, maybe not
int accumulate(int const* numbers, int first, int last)
{
// Are we good?
int r = 0;
for (int i = first; i <= last; i ++)
{
r += numbers[i];
}
return r;
}
int g(int const* p)
{
// Is this OK?
return accumulate(p, -1, 1);
}
it's a bug!
int accumulate(int const* numbers, int first, int last)
{
assert(first >= 0);
int r = 0;
for (int i = first; i <= last; i ++)
{
r += numbers[i];
}
return r;
}
int g(int const* p)
{
// Bug: -1 isn't in sequence that starts with p
return accumulate(p, -1, 1);
}
but...
int sample(int const* center, int first, int last)
{
// Are we good?
int r = 0;
for (int i = first; i <= last; i ++)
{
r += center[i];
}
return r;
}
int g(int const* p)
{
// Is this OK?
return sample(p, -1, 1);
}
what about now?
int sample(int const* center, int first, int last)
{
// First might be anything.
int r = 0;
for (int i = first; i <= last; i ++)
{
r += center[i];
}
return r;
}
int g(int const* p)
{
// center is not necessarily the start of the sequence.
return sample(p, -1, 1);
}
int accumulate_neighborhood(int const* position, int offset_first, int offset_last)
{
int r = 0;
for (int i = offset_first; i <= offset_last; i ++)
{
r += position[i];
}
return r;
}
int sample(int const* center, int first, int last)
{
return accumulate_neighborhood(center, first, last);
}
int accumulate_subrange(int const* numbers, int first, int last)
{
assert(first >= 0);
return accumulate_neighborhood(numbers, first, last);
}
what about now?
-isystem
.if
statements