| Document number | P3928R0 |
| Date | 2025-11-16 |
| Audience | SG9, LEWG |
| Reply-to | Hewill Kang <hewillk@gmail.com> |
static_sized_range
This paper introduces static_sized_range, a refinement of sized_range for
ranges whose sizes are known at compile time.
It allows detecting and retrieving a range's size as a constant expression through a new variable template
range_static_size_v with a similar naming style to range_size_t.
This enables compile-time reasoning about range sizes and improves constexpr support and optimization
opportunities in range adaptors and algorithms.
Initial revision.
The Ranges library initially attempted to identify ranges with compile-time known sizes using an exposition-only
concept, tiny-range:
template<auto> struct require-constant; // exposition only
template<class R>
concept tiny-range = // exposition only
sized_range<R> &&
requires { typename require-constant<remove_reference_t<R>::size()>; } &&
(remove_reference_t<R>::size() <= 1);
However, this approach relied on the presence of a static member size() and excluded many types, such
as
span<int, 1>. There was no general mechanism in the language to allow generic code to verify
whether
ranges::size(r) could be evaluated as a constant expression.
The acceptance of P2280 provided a new opportunity: by modifying the rules of constant evaluation, it allows expressions involving references to unknown objects to be considered valid constant expressions when the result does not depend on the object's identity.
This makes ranges::size(r) more generally usable at compile time and allows defining a concept like
static_sized_range in a simpler and more intuitive way.
Additionally, the C++26 simd specification heavily relies on this language enhancement: its wording
repeatedly checks that
ranges::size(r) is a constant expression to ensure, at compile time, the correctness of vector sizes.
However, these compile-time checks and optimizations should not be limited to simd. Introducing the
static_sized_range concept is therefore worthwhile, as it enables compile-time reasoning about range
sizes and broader use in generic programming.
This proposal introduces two related entities: the static_sized_range concept and the
range_static_size_v variable template.
The static_sized_range concept refines sized_range by further requiring that
ranges::size(r) be evaluable as a constant expression. It is defined as follows:
template<class T>
concept static_sized_range =
sized_range<T> && requires(T& t) { cw<ranges::size(t)>; };
Here, cw enables the formation of a constant_wrapper introduced in P2781 from an expression known to
be a
constant expression,
allowing the concept to directly check whether ranges::size(r) can be evaluated at compile time,
as enabled by P2280.
Once such a range is identified, its compile-time size can be retrieved through a variable template:
template<static_sized_range T>
constexpr auto range_static_size_v =
decltype([](T& t) { return cw<ranges::size(t)>; }(declval<T&>()))::value;
The definition relies on the same mechanism: it promotes the result of ranges::size(r) to a type via
cw, and then extracts the value from that type.
This effectively turns a compile-time constant into a type-level entity, allowing the variable template to retrieve
the constant directly through ordinary deduction.
Together, static_sized_range and range_static_size_v provide a minimal yet general mechanism
for compile-time reasoning about range extents:
static_assert(ranges::static_sized_range<array<int, 3>>); static_assert(ranges::range_static_size_v<array<int, 3>> == 3); static_assert(ranges::static_sized_range<span<int, 5>>); static_assert(ranges::range_static_size_v<span<int, 5>> == 5); static_assert(ranges::static_sized_range<ranges::single_view<int>>); static_assert(ranges::range_static_size_v<ranges::single_view<int>> == 1); static_assert(ranges::static_sized_range<ranges::empty_view<int>>); static_assert(ranges::range_static_size_v<ranges::empty_view<int>> == 0); static_assert(!ranges::static_sized_range<span<int>>); static_assert(!ranges::static_sized_range<vector<int>>); static_assert(!ranges::static_sized_range<optional<int>>); static_assert(ranges::range_static_size_v<string> == 42); // ill-formed: constraints not satisfied
Although range_static_size_v<R> exposes the size of a static_sized_range at compile
time, expressions such as
(range_static_size_v<R> >= 1) may not be constant expressions, since ranges::size(r)
can return an integral-class type lacking constexpr comparison or arithmetic operators.
LWG 4409 highlights this limitation, noting that such types may behave like integers at runtime but are not necessarily usable in constant expressions.
However, it is natural to expect that integer-class types should support compile-time
operations
like built-in integers. To align with this expectation,
it is desirable to clarify the wording for any integer-class type defined in [iterator.concept.winc], ensuring well-defined
constexpr behaviors.
ref_viewCurrently, ref_view<R>::size() simply forwards to the underlying range via
*r_:
constexpr auto size() const requires sized_range<R>
{ return ranges::size(*r_); }
Even if R satisfies static_sized_range,
this expression is still not a constant expression. As a result, for example,
ref_view<array<int, 42>> does not model static_sized_range.
This can be fixed by having size() return
range_static_size_v<R> when R models static_sized_range,
while keeping the original behavior for other ranges:
constexpr auto size() const requires sized_range<R> {
if constexpr (static_sized_range<R>)
return range_static_size_v<R>;
else
return ranges::size(*r_);
}
Similarly, empty() should receive similar treatment so that
ranges::empty on ref_view can also be a constant expression.
The effect of this change can be seen in the following example:
array a{1, 2, 3, 4, 5};
auto r = a | views::transform([](int i) { return i * i; })
| views::reverse;
static_assert(ranges::size(r) == 5); // ok
static_assert(!ranges::empty(r)); // ok
static_assert(ranges::static_sized_range<decltype(r)>); // ok
static_assert(ranges::range_static_size_v<decltype(r)> == 5); // ok
front/back across
array/span/view_interface
For view_interface, the front() and back() members can benefit from
static_sized_range. If a derived view models static_sized_range, calls on an empty range
can be rejected at compile time.
Similarly, array<T, N> and span<T, N> with a fixed extent can take advantage
of this: when N == 0, invoking front() or back() can be diagnosed at compile
time, providing safety without relying on runtime Hardened preconditions added in P3471.
join_view (LWG 4401)LWG 4401 observes that join_view is not
considered a sized_range,
even though in certain cases — such as when the outer range is sized and the inner range has a fixed size
— its total
size can be determined.
With the introduction of static_sized_range and range_static_size_v, this limitation can be
addressed more generally:
when the inner range models static_sized_range, the size of a join_view can be computed as
ranges::size(base_) * range_static_size_v<InnerRng>.
This effectively makes ranges::join_view<span<array<int, 3>, 4>>> a
static_sized_range with a static size of 12.
lazy_split_view (LWG 3855, 4108)
LWG 3855 notes that tiny-range is
limited and not fully general.
Using static_sized_range and range_static_size_v, we can rewrite tiny-range
with a concept that
correctly captures ranges whose size is known at compile time, and
a range with range_static_size_v<R> <= 1 naturally satisfies the intended semantics of
tiny-range
This means that lazy_split_view can now take an array or span of size 1 or 0 as the patterns, which is a nice enhancement:
array a{42};
auto r1 = views::istream<int>(in1) | views::lazy_split(a); // ok
auto r2 = views::istream<int>(in2) | views::lazy_split(array{42}); // ok
auto r3 = views::istream<int>(in3) | views::lazy_split(span{a}); // ok
LWG 4108 observes that lazy_split_view cannot
provide size() for certain valid cases. With static_sized_range, this can be improved:
when the
underlying range is sized and the pattern is statically empty, lazy_split_view can conditionally
provide
size() computed as ranges::size(base_).
span (LWG 4397, 4404)
LWG 4397 concerns constructing a span from a
statically sized range.
The standard currently enforces this with Hardened Preconditions at runtime to reject ranges whose size does
not match the fixed extent.
Applying static_sized_range allows this requirement to be expressed at compile time, which is a useful
enhancement.
LWG
4404 deals with class template argument deduction (CTAD) for
span(R&&).
When the underlying range has a statically known size, the current standard does not uniformly propagate this
information through CTAD due to language rule before P2280. With the newly
static_sized_range, the compiler can
conditionally treat such
span constructions as statically sized, enabling more precise compile-time checks and better
integration with
generic code.
simd/inplace_vector wording (LWG 4396)
In C++26 simd, vector sizes must be known at compile time so that the compiler can generate correct
SIMD
instructions. The standard currently expresses this requirement as "If ranges::size(r) is a constant
expression," which is verbose and not easily reusable in generic code.
With the introduction of static_sized_range, this check can be simplified to "If R models
static_sized_range".
This formulation is concise, clearly expresses the intent, and can be applied consistently across the standard
library.
Combined with range_static_size_v, it provides a uniform mechanism for compile-time size reasoning.
Note that the similar wording in LWG 4396 can also be
simplified with static_sized_range, making the intent explicit and consistent with
simd.
ranges::min/max/minmaxThose algorithms specify as a Preconditions that the input range must not be empty.
With the introduction of static_sized_range, these preconditions can be
verified at compile time.
For example, invoking ranges::min on
array<int, 0>
could be statically rejected, improving diagnostic clarity and program safety by turning a runtime undefined
behavior into a compile-time error.
Additionally, static_sized_range provides opportunities for compile-time optimizations, with many more
potential enhancements throughout the library.
For example, ranges::equal between two ranges of different static sizes can be evaluated to
false at compile time,
reducing unnecessary instantiations and runtime checks.
optional/inplace_vectorAfter
P3168, optional
could in principle be treated as a tiny-range,
since its maximum size is one. If it were considered
tiny-range, lazy_split_view could use an optional as the pattern
for
input ranges:
auto r = views::istream<int>(in) | views::lazy_split(optional{42});
However, tiny-range is intended to have a fully known, precise size at compile time to support
efficient code
generation, which optional does not provide. For this reason, the author does not adopt such a
treatment.
Additionally, similar to optional, inplace_vector also has a statically known maximum
size. In theory, this could be used to enhance range adaptors such as reserve_hint member introduced in
P2846:
for example, a join_view of a range of optional could have reserve_hint
returning ranges::size(base_) * 1, and ranges::size(base_) * N for range
of inplace_vector<T, N>.
However, this is a very specialized optimization and is not pursued here.
In theory, calls to front(), back(), or pop_back() on an
inplace_vector with zero capacity could be rejected at compile
time. However, this is an extremely limited case and does not justify adding such checks.
This wording is relative to Lastest Working Draft.
Add a new feature-test macro to 17.3.2 [version.syn]:
#define __cpp_lib_ranges_static_sized_range 2026XXL // freestanding, also in <ranges>
Modify 23.2.4 [sequence.reqmts] as indicated:
a.assign_range(rg)-60- Result:
void-61- Mandates:
[…]assignable_from<T&, ranges::range_reference_t<R>>is modeled. Forinplace_vector, ifRmodelsranges::static_sized_rangethenranges::size(rg)is a constant expressionranges::range_static_size_v<R>≤ranges::size(rg)a.max_size().a.front();-71- Result:
reference;const_referencefor constanta.-?- Mandates: For
array,a.empty()isfalse.-72- Hardened preconditions:
a.empty()isfalse.[…]
a.back();-75- Result:
reference;const_referencefor constanta.-?- Mandates: For
array,a.empty()isfalse.-76- Hardened preconditions:
a.empty()isfalse.
Modify 23.3.16.2 [inplace.vector.cons] as indicated:
template<container-compatible-range<T> R> constexpr inplace_vector(from_range_t, R&& rg);-?- Mandates: If
Rmodelsranges::static_sized_rangethenranges::size(rg)is a constant expressionranges::range_static_size_v<R>≤ranges::size(rg)N.-9- Effects: Constructs an
inplace_vectorwith the elements of the rangerg.-10- Complexity: Linear in
ranges::distance(rg).
Modify 23.7.2.2.2 [span.cons] as indicated:
template<class R> constexpr explicit(extent != dynamic_extent) span(R&& r);-?- Mandates: If
extentis not equal todynamic_extentandRmodelsranges::static_sized_range, thenranges::range_static_size_v<R> == extent.-16- Constraints: Let
Uberemove_reference_t<ranges::range_reference_t<R>>. […]
Modify 23.7.2.2.3 [span.deduct] as indicated:
template<class R> span(R&&) -> see belowspan<remove_reference_t<ranges::range_reference_t<R>>>;-2- Constraints:
Rsatisfiesranges::contiguous_range.-?- Remarks: Let
Tdenote the typeremove_reference_t<ranges::range_reference_t<R>>. The deduced type isspan<T, static_cast<size_t>(ranges::range_static_size_v<R>)>ifRmodelsranges::static_sized_range, otherwisespan<T>.
Modify 23.7.2.2.6 [span.elem] as indicated:
constexpr reference front() const;-?- Mandates: If
extentis not equal todynamic_extent, thenempty()isfalse.-6- Hardened preconditions:
empty()isfalse.[…]
constexpr reference back() const;-?- Mandates: If
extentis not equal todynamic_extent, thenempty()isfalse.-9- Hardened preconditions:
empty()isfalse.
Modify 24.3.4.4 [iterator.concept.winc] as indicated:
-9- All integer-class types model
regular([concepts.object]) andthree_way_comparable<strong_ordering>([cmp.concept]).-?- Operations on integer-class type shall be usable in constant expressions.
Modify 25.2 [ranges.syn] as indicated:
// mostly freestanding #include <compare> // see [compare.syn] #include <initializer_list> // see [initializer.list.syn] #include <iterator> // see [iterator.synopsis] namespace std::ranges { […] // [range.sized], sized ranges template<class> constexpr bool disable_sized_range = false; template<class T> concept approximately_sized_range = see below; template<class T> concept sized_range = see below; template<class T> concept static_sized_range = see below; template<static_sized_range T> constexpr auto range_static_size_v = see below; […] }
Add 25.4.? Static sized ranges [range.static.sized] after 25.4.4 [range.sized] as indicated:
-1- The
static_sized_rangeconcept refinessized_rangewith the requirement that the number of elements in the range can be determined in compile-time usingranges::range_static_size_v.template<class T> concept static_sized_range = sized_range<T> && requires(T& t) { cw<ranges::size(t)>; }; auto get-size-cw(auto& t) // exposition only -> decltype(cw<ranges::size(t)>); template<static_sized_range T> constexpr auto range_static_size_v = decltype(get-size-cw(declval<T&>()))::value;
Modify 25.5.3.2 [view.interface.members] as indicated:
constexpr decltype(auto) front() requires forward_range<D>; constexpr decltype(auto) front() const requires forward_range<const D>;-?- Mandates: Let
Rbedecltype(derived()). IfRmodelsranges::static_sized_range, thenranges::range_static_size_v<R> > 0.
-1- Hardened preconditions:
!empty()istrue.-2- Effects: Equivalent to:
return *ranges::begin(derived());
constexpr decltype(auto) back() requires bidirectional_range<D> && common_range<D>; constexpr decltype(auto) back() const requires bidirectional_range<const D> && common_range<const D>;-?- Mandates: Let
Rbedecltype(derived()). IfRmodelsranges::static_sized_range, thenranges::range_static_size_v<R> > 0.
-3- Hardened preconditions:
!empty()istrue.
Edit 25.7.6.2 [range.ref.view] as indicated:
namespace std::ranges {
template<range R>
requires is_object_v<R>
class ref_view : public view_interface<ref_view<R>> {
[…]
public:
[…]
constexpr bool empty() const
requires requires { ranges::empty(*r_); } {
if constexpr (static_sized_range<R>)
return range_static_size_v<R> == 0;
else
return ranges::empty(*r_);
}
constexpr auto size() const requires sized_range<R> {
if constexpr (static_sized_range<R>)
return range_static_size_v<R>;
else
return ranges::size(*r_);
}
[…]
};
[…]
}
Modify 25.7.14.2 [range.join.view] as indicated:
namespace std::ranges {
template<input_range V>
requires view<V> && input_range<range_reference_t<V>>>
class join_view : public view_interface<join_view<V>> {
[…]
public:
[…]
constexpr auto size()
requires sized_range<V> &&
static_sized_range<InnerRng> {
using CT = common_type_t<range_size_t<V>, range_size_t<InnerRng>>;
return CT(ranges::size(base_)) * CT(range_static_size_v<InnerRng>);
}
constexpr auto size() const
requires sized_range<const V> &&
static_sized_range<range_reference_t<const V>> {
using InnerConstRng = range_reference_t<const V>;
using CT = common_type_t<range_size_t<const V>, range_size_t<InnerConstRng>>;
return CT(ranges::size(base_)) * CT(range_static_size_v<InnerConstRng>);
}
};
[…]
}
Modify 25.7.15.2 [range.join.with.view] as indicated:
namespace std::ranges {
[…]
template<input_range V, forward_range Pattern>
requires view<V> && input_range<range_reference_t<V>>
&& view<Pattern>
&& concatable<range_reference_t<V>, Pattern>
class join_with_view : public view_interface<join_with_view<V, Pattern>> {
[…]
public:
[…]
constexpr auto size()
requires sized_range<V> &&
sized_range<Pattern> &&
static_sized_range<InnerRng> {
using CT = common_type_t<
range_size_t<V>, range_size_t<InnerRng>, range_size_t<Pattern>>;
const auto base_size = ranges::size(base_);
if (base_size == 0)
return CT(0);
return CT(base_size) * CT(range_static_size_v<InnerRng>) +
CT(base_size - 1) * CT(ranges::size(pattern_));
}
constexpr auto size() const
requires sized_range<const V> &&
sized_range<const Pattern> &&
static_sized_range<range_reference_t<const V>> {
using InnerConstRng = range_reference_t<const V>;
using CT = common_type_t<
range_size_t<const V>, range_size_t<InnerConstRng>, range_size_t<const Pattern>>;
const auto base_size = ranges::size(base_);
if (base_size == 0)
return CT(0);
return CT(base_size) * CT(range_static_size_v<InnerConstRng>) +
CT(base_size - 1) * CT(ranges::size(pattern_));
}
};
[…]
}
Modify 25.7.16.2 [range.lazy.split.view] as indicated:
namespace std::ranges {
template<auto> struct require-constant; // exposition only
template<class R>
concept tiny-range = // exposition only
static_sized_range<R> &&
requires { typename require-constant<remove_reference_t<R>::size()>; } &&
(range_static_size_v<R>remove_reference_t<R>::size() <= 1);
template<input_range V, forward_range Pattern>
requires view<V> && view<Pattern> &&
indirectly_comparable<iterator_t<V>, iterator_t<Pattern>, ranges::equal_to> &&
(forward_range<V> || tiny-range<Pattern>)
class lazy_split_view::view_interface<lazy_split_view<V, Pattern>> {
[…]
constexpr auto size()
requires sized_range<V> &&
static_sized_range<Pattern> &&
(range_static_size_v<Pattern> == 0)
{ return ranges::size(base_); }
constexpr auto size() const
requires sized_range<const V> &&
static_sized_range<Pattern> &&
(range_static_size_v<Pattern> == 0)
{ return ranges::size(base_); }
};
[…]
}
Modify 25.7.16.5 [range.lazy.split.inner] as indicated:
constexpr inner-iterator& operator++();-5- Effects: Equivalent to:
incremented_ = true; if constexpr (!forward_range<Base>) { if constexpr (range_static_size_v<Pattern>Pattern::size()== 0) { return *this; } } ++i_.current; return *this;
Modify 25.7.17.2 [range.split.view] as indicated:
namespace std::ranges {
template<forward_range V, forward_range Pattern>
requires view<V> && view<Pattern> &&
indirectly_comparable<iterator_t<V>, iterator_t<Pattern>, ranges::equal_to>
class split_view : public view_interface<split_view<V, Pattern>> {
[…]
constexpr auto size()
requires sized_range<V> &&
static_sized_range<Pattern> &&
(range_static_size_v<Pattern> == 0) {
return ranges::size(base_);
}
constexpr auto size() const
requires sized_range<const V> &&
static_sized_range<Pattern> &&
(range_static_size_v<Pattern> == 0) {
return ranges::size(base_);
}
};
[…]
}
Modify 26.8.9 [alg.min.max] as indicated:
template<class T> constexpr T min(initializer_list<T> r); […] template<execution-policy Ep, sized-random-access-range R, class Proj = identity, indirect_strict_weak_order<projected<iterator_t<R>, Proj>> Comp = ranges::less> requires indirectly_copyable_storable<iterator_t<R>, range_value_t<R>*> range_value_t<R> ranges::min(Ep&& exec, R&& r, Comp comp = {}, Proj proj = {});[…]-?- Mandates: If
Rmodelsranges::static_sized_range, thenranges::range_static_size_v<R> > 0.-5- Preconditions:
ranges::distance(r)>0. […]template<class T> constexpr T max(initializer_list<T> r); […] template<execution-policy Ep, sized-random-access-range R, class Proj = identity, indirect_strict_weak_order<projected<iterator_t<R>, Proj>> Comp = ranges::less> requires indirectly_copyable_storable<iterator_t<R>, range_value_t<R>*> range_value_t<R> ranges::max(Ep&& exec, R&& r, Comp comp = {}, Proj proj = {});[…]-?- Mandates: If
Rmodelsranges::static_sized_range, thenranges::range_static_size_v<R> > 0.-13- Preconditions:
ranges::distance(r)>0. […]template<class T> constexpr pair<T, T> minmax(initializer_list<T> t); […] template<execution-policy Ep, sized-random-access-range R, class Proj = identity, indirect_strict_weak_order<projected<iterator_t<R>, Proj>> Comp = ranges::less> requires indirectly_copyable_storable<iterator_t<R>, range_value_t<R>*> ranges::minmax_result<range_value_t<R>> ranges::minmax(Ep&& exec, R&& r, Comp comp = {}, Proj proj = {});-?- Mandates: If
Rmodelsranges::static_sized_range, thenranges::range_static_size_v<R> > 0.-21- Preconditions:
ranges::distance(r)>0. […]
Modify 29.10.7.2 [simd.ctor] as indicated:
template<class R, class... Flags> constexpr basic_vec(R&& r, flags<Flags...> = {}); template<class R, class... Flags> constexpr basic_vec(R&& r, const mask_type& mask, flags<Flags...> = {});-12- Let
maskbemask_type(true)for the overload with nomaskparameter.-13- Constraints:
[…]
(13.1) —
Rmodelsranges::contiguous_rangeandranges::static_sized_range,
(13.2) —ranges::size(r)is a constant expression,(13.3) —
ranges::range_static_size_v<R>is equal toranges::size(r)size(), and(13.4) —
ranges::range_value_t<R>is a vectorizable type and satisfiesexplicitly-convertible-to<T>.template<class R, class... Ts> basic_vec(R&& r, Ts...) -> see below;-17- Constraints:
(17.1) —Rmodelsranges::contiguous_rangeandranges::static_sized_range., and
(17.2) —ranges::size(r)is a constant expression.-18- Remarks: The deduced type is equivalent to
vec<ranges::range_value_t<R>, static_cast<simd-size-type>(.ranges::range_static_size_v<R>ranges::size(r))>
Modify 29.10.8.6 [simd.loadstore] as indicated:
template<class V = see below, ranges::contiguous_range R, class... Flags> requires ranges::sized_range<R> constexpr V unchecked_load(R&& r, flags<Flags...> f = {}); […] template<class V = see below, contiguous_iterator I, sized_sentinel_for<I> S, class... Flags> constexpr V unchecked_load(I first, S last, const typename V::mask_type& mask, flags<Flags...> f = {});-1- Let […]
-2- Mandates: If
Rmodelsranges::static_sized_rangethenranges::size(r)is a constant expressionranges::range_static_size_v<R>≥ranges::size(r)V::size().
[…]template<class T, class Abi, ranges::contiguous_range R, class... Flags> requires ranges::sized_range<R> constexpr void unchecked_store(const basic_vec<T, Abi>& v, R&& r, flags<Flags...> f = {}); […] template<class T, class Abi, contiguous_iterator I, sized_sentinel_for<I> S, class... Flags> constexpr void unchecked_store(const basic_vec<T, Abi>& v, I first, S last, const typename basic_vec<T, Abi>::mask_type& mask, flags<Flags...> f = {});-11- Let […]
-22- Mandates: If
Rmodelsranges::static_sized_rangethenranges::size(r)is a constant expressionranges::range_static_size_v<R>≥ranges::size(r)simd-size-v<T, Abi>.