If we look at the following program, we'll notice several problems. Comments will explicitly explain them.
The two biggest issues are
1) no easy way to convert a large array into a smaller array
2) Compilers don't need to error when the size is known and a literal index is outside of it
#include <cstddef>
void test16(char (&arr)[16]) { arr[15] = 0x12; }
void test32(char (&arr)[32]) { arr[31] = 0x34; }
// Sanitizers won't catch this if you change 257 to 255
void test256(char (&arr)[256]) { arr[257] = 0x56; }
template <size_t N> void testN(char (&arr)[N]) {
//test16(arr); // error because size is not exact
test32(arr); // exact, compiles
// Typecasting is bad, as we know
test16(reinterpret_cast<char (&)[16]>(arr)); // compiles
test256(reinterpret_cast<char (&)[256]>(arr)); // compiles and will overwrite memory
}
int main() {
char buf[32]{};
testN(buf); // two problems, 32 is smaller than 256 and
// test256 writes to 257, which is clearly out of range
// the below doesn't cause a warning (or error) in some compilers
buf[-1] = 0x78;
// I much rather the previous line be written as
*(buf-1) = 0x78;
}
You can fix one of those issues by switching away from "reference to array" and toward "std::span":
void test16(std::span<char, 16> arr) { arr[15] = 0x12; }
void test32(std::span<char, 32> arr) { arr[31] = 0x34; }
template <size_t N> void testN(char (&arr)[N]) {
//test16(arr); // error because size is not exact
test32(arr); // exact, compiles
// Typecasting is bad, as we know
test16(std::span(arr).template first<16>()); // compiles
test256(std::span(arr).template first<256>()); // NO LONGER compiles
}
However, this is ugly — it requires ".template" — and it doesn't scale or compose — you can't replace `testN` with
template<size_t N> void testN(std::span<char, N> arr)
because the `N` will not be deducible in a call like `testN(buf)`.
Also, no mainstream compiler today seems to be aware of `span` as a special case in their bounds-checking code: every compiler will warn about
void test256(char (&arr)[256]) { arr[257] = 0x56; } // warning
but no compiler will warn about
void test256(std::span<char, 256> arr) { arr[257] = 0x56; } // no warning
So indeed `span` isn't a magic bullet for the bounds-checking problem in C++. But the problem has still been reasonably well solved for half a century: all you have to do is pass (pointer, length) or (first, last) rather than just (pointer). Don't try to pass around references to arrays; as you've noticed, that doesn't really work because most ranges are not precisely arrays of a given length. But you can write things like:
void test16(char *p, int n) {
assert(n >= 16);
p[15] = 0x12;
}
void testN(char *p, int n) {
test16(arr, n);
~~~~
}
and I'd expect a static analyzer or linter to be able to deal with that code pretty well.
By having 'arr[257]' and 'buf[-1]' become an error, obvious mistakes will be caught immediately.
I suggest you file a bug with the compiler vendor if you find any compilers where
void f(char (&arr)[256]) { arr[257] = 1; }
doesn't give a warning with -Wall -Wextra. (That means file one against GCC, because they warn only if you also pass -O2, and they emit the warning as -Warray-bounds, which is explicitly turned off in most industry projects I've seen, due to its high false-positive rate!)
To allow testN to be implemented without casting, I suggest a syntax
How about a simple wrapper, such as:
template<class T, size_t N>
struct APO {
T *a_;
template<size_t M> using Ref = T(&)[M];
template<size_t M> requires (M <= N)
operator Ref<M>() const {
return *reinterpret_cast<T(*)[M]>(a_);
}
};
template<class T, size_t N>
auto anyPrefixOf(T (&a)[N]) {
return APO<T, N>{a};
}