C++ Logo

std-proposals

Advanced search

Re: [std-proposals] ranges::size should always return a unsigned integer

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Wed, 8 Oct 2025 16:31:18 -0400
On Wed, Oct 8, 2025 at 11:32 AM Robin Savonen Söderholm via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> On Wed, Oct 8, 2025, 17:23 Yexuan Xiao via Std-Proposals <
> std-proposals_at_[hidden]> wrote:
>
>> Why is it allowed to return a signed integer? C++ users are accustomed to
>> the fact that the size_type of containers is an unsigned integer, so the
>> size of containers should also return an unsigned integer. sizes that
>> return negative numbers are pathological and cannot be used. The current
>> specification forces writers of generic algorithms to convert the result to
>> unsigned on every call to ranges::size, which is something ranges::size
>> should have taken care of. If they don't do this, the compiler will
>> complain about signed and unsigned mismatches. C++20 also added
>> ranges::ssize, which always returns a signed integer. If generic algorithms
>> prefer signed integers, they can use ranges::ssize. Therefore, it makes no
>> sense for `ranges::size` to preserve the signedness of the return type of
>> containers' size. Does C++ need ranges::usize? I believe the answer is
>> clearly no. If C++ doesn't fix ranges::size in a timely manner, perhaps one
>> day users will invent ranges::usize themselves and then tell others not to
>> use ranges::size
>>
>
> I disagree, I prefer using 'std::ssize'/'std::ranges::ssize' because I
> want to have it always signed. There seems to be a strong consensus that
> std::size_t should had been signed from the beginning.
>

Robin, you're not disagreeing. Yexuan isn't saying "let's eliminate
ranges::ssize." He's saying we should apply this patch to [range.prim.size]
<https://eel.is/c++draft/range.prim.size>:

Given a subexpression E with type T, let t be an lvalue that denotes the
reified object for E. Then:
(2.1) If T is an array of unknown bound ([dcl.array]), ranges::size(E) is
ill-formed.
(2.2) Otherwise, if T is an array type, ranges::size(E) is
expression-equivalent to auto *to-unsigned-like *(extent_v<T>).
(2.3) Otherwise, if disable_sized_range<remove_cv_t<T>> is false and auto
*to-unsigned-like *(t.size()) is a valid expression of integer-like type,
ranges::size(E) is expression-equivalent to auto *to-unsigned-like *
(t.size()).
(2.4) Otherwise, if T is a class or enumeration type,
disable_sized_range<remove_cv_t<T>> is false, and auto
*to-unsigned-like *(size(t))
is a valid expression of integer-like type where the meaning of `size` is
established as-if by performing argument-dependent lookup only, then
ranges::size(E) is expression-equivalent to that expression.
(2.5) Otherwise, if *to-unsigned-like*(ranges::end(t) - ranges::begin(t))
is a valid expression and the types I and S of ranges::begin(t) and
ranges::end(t) (respectively) model both sized_sentinel_for<S, I> and
forward_iterator<I>, then ranges::size(E) is expression-equivalent to
*to-unsigned-like*(ranges::end(t) - ranges::begin(t)).
(2.6) Otherwise, ranges::size(E) is ill-formed.

This seems nicely consistent to me: right now (2.[234]) are arguably
inconsistent with (2.5).
However, it opens a can of worms: should we also change [iterator.range]
<https://eel.is/c++draft/iterator.range> like this, so that `std::size`
will be consistent with `std::ranges::size`?

    template<class C> constexpr auto size(const C& c)
        noexcept(noexcept( *to-unsigned-like(* c.size() *)* )) -> decltype(
*to-unsigned-like(* c.size() *)* );
    Returns: *to-unsigned-like(* c.size() *)* .

But that makes `std::size` inconsistent with `std::ssize`, so should it
be...?

    template<class C> constexpr auto size(const C& c)
        noexcept(noexcept( c.size() )) -> *common_type_t<size_t,
make_unsigned_t<*decltype(c.size())*>>*;
    Returns: c.size().
    *Equivalent to: return static_cast<common_type_t<size_t,
make_unsigned_t<decltype(c.size())>>>(c.size());*

Just a big can of worms. And for what? so that people can write and use
containers with signed `.size()` methods while simultaneously writing code
that (A) uses `ranges::size` but (B) somehow fails to handle signed sizes
correctly? That doesn't seem like a big benefit; and there's certainly a
cost — complicating the specifications, introducing inconsistencies one way
or another, introducing more heavyweight dependencies (although `ssize`
already pulls in these dependencies, so I don't expect them to *really*
cost anything extra here) in what *should* be a *really simple* function.

So I'm inclined to agree with jwakely: Just let sleeping dogs lie, and
follow Postel's Law: don't write STL-style containers with signed sizes,
and *do* write STL-style *algorithms* that can handle signed sizes, and
you'll be fine.

Tangent: I do wonder how many library vendors test all their std::ranges
algorithms with signed sizes. If you could prove that several vendors have
actual bugs in this area — that it's actually difficult to write STL-style
algorithms that can handle a signed result from `ranges::size` — then you'd
have a stronger case, IMHO. But off the top of my head, I can't even
imagine what such a bug would look like.

–Arthur

P.S. — The `noexcept` clauses above come from the
approved-but-not-yet-merged P3016R6
<https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3016r6.html>.

Received on 2025-10-08 20:31:36