C++ Logo

std-proposals

Advanced search

[std-proposals] Signed sizes

From: Jeremy Rifkin <rifkin.jer_at_[hidden]>
Date: Mon, 9 Dec 2024 22:01:44 -0600
Hello,
Signed and unsigned integers do not mix well in C++. Often when
working with standard library containers I end up having to explicitly
static_cast between signed and unsigned a lot which clutters my code
and makes many operations less ergonomic (I use -Wsign-conversion due
to the bug-prone nature of such conversions). There has been extensive
discussion on this previously and from what I have read and researched
there seems to be growing consensus that, in hindsight, unsigned sizes
and indexes were a mistake.

There have been nudges in the direction of using signed more over
unsigned, for example std::ssize(), std::views::enumerate's signed
index, and the initial design of std::span was fully signed. However,
there has been no decisive push to change the status quo to be more
signed-friendly which results in a lot of mixing between signed an
unsigned integers. std::span's design was changed for consistency with
the rest of the standard library to not further exacerbate mixing
issues. I've not found any discussion regarding the decision to use a
signed integer for std::views::enumerate.

Relevant papers I came across:
- P0330, Literal Suffixes for ptrdiff_t and size_t,
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0330r4.html
- P1227, Signed ssize() functions, unsigned size() functions,
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1227r2.html
- P1491, Don’t add to the signed/unsigned mess,
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1491r0.pdf
- P1428, Subscripts and sizes should be signed,
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1428r0.pdf
- P1523, Views and Size Types,
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1523r1.pdf
- P1970, Consistency for size() functions,
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p1970r1.html

If the standard library was consistent with always using unsigned
integers for sizes and indexes I would personally come across fewer
pain points, however, the inconsistency of std::views::enumerate means
that even idiomatic usage of enumerate requires mixing signed and
unsigned integers.

In the specific case of std::views::enumerate, one option would be to
add a variation that produces unsigned indexes. This, plasters over
the underlying issue and could lead ot more confusion about mixing,
however, it would alleviate some issues.

The other option would be a bigger plan to deprecate and remove
unsigned indexing APIs and replace them with signed APIs:

At standard revision X: Deprecate all unsigned interfaces, add a
signed counterpart. I think these functions would have to be templated
for signed types in order to eliminate ambiguity between signed and
unsigned candidates:

template<typename T>
concept signed_integral = std::integral<T> && std::is_signed_v<T>;

- auto& operator[](std::size_t) { ... }
- auto& operator[](std::size_t) const { ... }
+ [[deprecated]] auto& operator[](std::size_t) {}
+ [[deprecated]] auto& operator[](std::size_t) const {}
+ template<signed_integral I>
+ auto& operator[](I i) { ... }
+ template<signed_integral I>
+ auto& operator[](I i) const { ... }

- auto& at(std::size_t) { ... }
- auto& at(std::size_t) const { ... }
+ [[deprecated]] auto& at(std::size_t) { ... }
+ [[deprecated]] auto& at(std::size_t) const { ... }
+ template<signed_integral I>
+ auto& at(I i) { ... }
+ template<signed_integral I>
+ auto& at(I i) const { ... }

etc

At standard revision X + 1: Remove the deprecated unsigned interfaces,
remove the template, replace with a signed non-template function:

- [[deprecated]] auto& operator[](std::size_t) {}
- [[deprecated]] auto& operator[](std::size_t) const {}
- template<signed_integral I>
- auto& operator[](I i) { ... }
- template<signed_integral I>
- auto& operator[](I i) const { ... }
+ [[deprecated]] auto& operator[](std::ptrdiff_t) {}
+ [[deprecated]] auto& operator[](std::ptrdiff_t) const {}

- [[deprecated]] auto& at(std::size_t) { ... }
- [[deprecated]] auto& at(std::size_t) const { ... }
- template<signed_integral I>
- auto& at(I i) { ... }
- template<signed_integral I>
- auto& at(I i) const { ... }
+ [[deprecated]] auto& at(std::ptrdiff_t) { ... }
+ [[deprecated]] auto& at(std::ptrdiff_t) const { ... }

etc

This would of course be a massive change. Past deprecation / removals
have been much more localized.

One thing I'm not sure about is whether the X+1 step of removing the
`template<signed_integral I>` interface and replacing it with a
concrete function could cause problems with ABI or linkage. If this is
an ABI issue, X+1 could be adjusted to leave the signed template and
not add in the non-template counterpart, which isn't ideal but would
be effective none the less.

I'd love to hear thoughts and feedback on pursuing something like
this. I'm sure there are complexities I have not thought of. If this
is too ambitious, I'd love to hear that too.

Cheers,
Jeremy

Received on 2024-12-10 04:01:57