• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1+++
2title = "Narrow contracts"
3description = "Describes narrow-contract functions that do not work for all input values, and advantage of using them."
4weight = 60
5+++
6
7A program's thread of execution can enter a "disappointing" state for two reasons:
8
9* due to disappointing situation in the environment (operating system, external input),
10  or
11* due to a bug in the program.
12
13The key to handling these disappointments correctly is to identify to which
14category they belong, and use the tools adequate for a given category. In this
15tutorial when we say "error" or "failure" we only refer to the first category.
16A bug is not an error.
17
18A bug is when a program is something else than what it is supposed to be. The
19correct action in that case is to change the program so that it is exactly what
20it is supposed to be. Unfortunately, sometimes the symptoms of a bug are only
21detected when the system is running and at this point no code changes are possible.
22
23In contrast, a failure is when a correct function in a correct program reflects
24some disappointing behavior in the environment. The correct action in that case
25is for the program to take a control path different than usual, which will likely
26cancel some operations and will likely result in different communication with the
27outside world.
28
29Symptoms of bugs can sometimes be detected during compilation or static program
30analysis or at run-time when observing certain values of objects that are declared
31never to be valid at certain points. One classical example is passing a null pointer
32to functions that expect a pointer to a valid object:
33
34```c++
35int f(int * pi) // expects: pi != nullptr
36{
37  return *pi + 1;
38}
39```
40
41Passing a null pointer where it is not expected is so common a bug that tools
42are very good at finding them. For instance, static analyzers will usually detect
43it without even executing your code. Similarly, tools like undefined behavior
44sanitizers will compile a code as the one above so that a safety check is performed
45to check if the pointer is null, and an error message will be logged and program
46optionally terminated.
47
48More, compilers can perform optimizations based on undefined behavior caused by
49dereferencing a null pointer. In the following code:
50
51```c++
52pair<int, int> g(int * pi) // expects: pi != nullptr
53{
54  int i = *pi + 1;
55  int j = (pi == nullptr) ? 1 : 0;
56  return {i, j};
57}
58```
59
60The compiler can see that if `pi` is null, the program would have undefined
61behavior. Since undefined behavior is required by the C++ standard to never
62be the programmer's intention, the compiler
63assumes that apparently this function is never called with `pi == nullptr`. If so,
64`j` is always `0` and the code can be transformed to a faster one:
65
66```c++
67pair<int, int> g(int * pi) // expects: pi != nullptr
68{
69  int i = *pi + 1;
70  int j = 0;
71  return {i, j};
72}
73```
74
75Functions like the one above that declare that certain values of input parameters
76must not be passed to them are said to have a *narrow contract*.
77
78Compilers give you non-standard tools to tell them about narrow contracts, so
79that they can detect it and make use of it the same way as they are detecting
80invalid null pointers. For instance, if a function in your library takes an `int`
81and declares that the value of this `int` must never be negative. You can use
82`__builtin_trap()` available in GCC and clang:
83
84```c++
85void h(int i) // expects: i >= 0
86{
87  if (i < 0) __builtin_trap();
88
89  // normal program logic follows ...
90}
91```
92
93This instruction when hit, causes the program to exit abnormally, which means:
94
95* a debugger can be launched,
96* static analyzer can warn you if it can detect a program flow that reaches this
97  point,
98* UB-sanitizer can log error message when it hits it.
99
100Another tool you could use is `__builtin_unreachable()`, also available in GCC
101and clang:
102
103```c++
104void h(int i) // expects: i >= 0
105{
106  if (i < 0) __builtin_unreachable();
107
108  // normal program logic follows ...
109}
110```
111
112This gives a hint to the tools: the programmer guarantees that the program flow
113will never reach to the point of executing it. In other words, it is undefined
114behavior if control reaches this point. Compiler and other tools can take this
115for granted. This way they can deduce that expression `i < 0` will never be true,
116and they can further use this assumption to issue warnings or to optimize the code.
117UB-sanitizers can use it to inject a log message and terminate if this point is
118nonetheless reached.
119
120Allowing for some input values to be invalid works similarly to cyclic redundancy
121checks. It allows for the possibility to observe the symptoms of the bugs (not
122the bugs themselves), and if the symptom is revealed the hunt for the bug can start.
123This is not only tools that can now easily detect symptoms of bugs, but also
124humans during the code review. A reviewer can now say, "hey, function `h()` is
125expecting a non-negative value, but this `i` is actually `-1`; maybe you wanted
126to pass `j` instead?".
127