C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Error on out-of-bounds index, and syntax for conversion

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Sat, 23 Aug 2025 10:43:06 -0400
On Fri, Aug 22, 2025 at 4:04 PM Levo D via Std-Proposals <
std-proposals_at_[hidden]> wrote:

>
> 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":
https://godbolt.org/z/cWjj3sz3n

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:

// https://godbolt.org/z/z8GKfhsf4

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};
}

template<size_t N>
void testN(char (&arr)[N]) {
  test16(anyPrefixOf(arr)); // OK
  test32(anyPrefixOf(arr)); // OK
  test256(anyPrefixOf(arr)); // Error
}

HTH,
Arthur

Received on 2025-08-23 14:43:25