Range Constrained Types in C++

One of the most common software bugs is the humble buffer overflow. In its simplest form this can happen when an array is accessed out of bounds:

cpp
 1 | int arr[5];
 2 | 
 3 | int foo(int i) {
 4 |     return arr[i];
 5 | }
 6 | 
 7 | int main() {
 8 |     return foo(5);
 9 | }

The code above causes 🌩Undefined Behavior🌩TM (or UB for short). That is to say that a standard-compliant compiler can produce code that does literally anything. Roughly speaking this is to give the compiler more leeway in making aggressive optimizations: it means that it can assume UB never happens and optimize the code based on that assumption.

So more likely than not foo will simply compile to machine code that reads and returns the memory at &arr + i (in fact you can click the little green “Run”-button in the corner of the code to see that clang’s codegen goes exactly that). Since the array only occupies memory up to &arr + 4, calling foo like above will try to read the memory of whatever object the compiler decided to place after arr. If that object is always the same, then executing the binary will consistently produce the same return code. Or maybe there is no object after arr and the address right after arr is not mapped to a region that the created process is allowed to read, resulting in a SEGFAULT.

std::array

In this respect C-style arrays are considered unsafe, and many “modern” C++ style guides suggest using std::array instead. Objects of this type implement the at function which checks that the supplied index is within the bounds of the array. If it isn’t, it throws an exception.

cpp
 1 | #include <array>
 2 | 
 3 | std::array<int, 5> arr;
 4 | 
 5 | int foo(int i) {
 6 |     return arr.at(i);
 7 | }
 8 | 
 9 | int main() {
10 |     return foo(5);
11 | }