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