Document #: | P2393R0 |
Date: | 2021-06-12 |
Project: | Programming Language C++ |
Audience: |
LWG |
Reply-to: |
Tim Song <t.canens.cpp@gmail.com> |
This paper revamps the specification and use of integer-class types to resolve a number of issues, including [LWG3366] and [LWG3376].
Integer-class types, introduced in [P1522R1], are implementation-defined class types that are supposed to “behave as integers do”. Unfortunately, they are not yet required to do that, and the failure to do so leads to a number of issues in the library specification. For example:
common_type
, which causes issues in various ranges components such as zip_view
([P2321R1]) and join_view
(24.7.11.3 [range.join.iterator]) that uses common_type_t
to determine the difference type of a range resulting from the composition of multiple ranges.ranges::equal
) that may need to compare distances obtained from two ranges can even be implemented.Additionally, there’s a pervasive problem in the ranges and algorithms clauses in that the wording fails to take into account the fact that random access iterator operations are only required to work with the iterator’s difference type, and not any other integer-like type. This was not a major issue in practice before C++20 because reasonable users (and even somewhat less reasonable ones) do not normally go out of the way to define deleted overloads for other integer types (or constrain their overload to require exactly the same type), but now that integer-class types can require explicit conversions, things are more problematic.
First, tighten the specification of integer-class types to mandate support for what algorithms and range adaptors require, and clean up several library issues in the process.
common_type_t
to compute the difference type of a range. (Only the signed case is considered because a) there is no corresponding use case for unsigned and b) the property doesn’t hold for some built-in unsigned integer types that could be promoted to a signed type by integral promotion.)Next, clean up the ranges wording (again) to explicitly cast to the difference type where required.
Finally, add blanket wording in the algorithms clause that i + n
and i - n
for iterator i
and integer-like type n
in the wording behave as-if n
is first cast to i
’s difference type. This allows the spec to have things like first1 + (last2 - first2)
without worrying about having to cast them (a fully correct implementation would still need to cast, however).
This wording is relative to [N4885] after the application of [P2367R0].
[ Drafting note: Because integer-class type is only used in this subclause (the rest of the standard uses integer-like), I did not rename it. Cf.
enum class
. ][ Editor's note: P2321R2 § 5.4 also contains edits to this subclause. The wording below subsumes those edits and should be applied instead if both papers are adopted. ]
2 A type
I
is an integer-class type if it is in a set of implementation-defined class types that behave as integer types do, as defined below. [ Note ?: An integer-class type is not necessarily a class type. — end note ]3 The range of representable values of an integer-class type is the continuous set of values over which it is defined. The values 0 and 1 are part of the range of every integer-class type. If any negative numbers are part of the range, the type is a signed-integer-class type; otherwise, it is an unsigned-integer-class type. For any integer-class type, its range of representable values is either -2N-1 to 2N-1-1 (inclusive) for some integer N, in which case it is a signed-integer-class type, or 0 to 2N-1 (inclusive) for some integer N, in which case it is an unsigned-integer-class type. In both cases, N is called the width of the integer-class type. The width of an integer-class type is greater than that of every integral type of the same signedness.
[ Editor's note: Move paragraph 11 here to make integer-like available for use in subsequent wording. ]
11 A type
I
other than cvbool
is integer-like if it modelsintegral<I>
or if it is an integer-class type. An integer-like type I is signed-integer-like if it modelssigned_integral<I>
or if it is a signed-integer-class type. An integer-like typeI
is unsigned-integer-like if it modelsunsigned_integral<I>
or if it is an unsigned-integer-class type.[ Drafting note: The unique addition ensures that even if an implementation decides to provide two integer-class types of the same signedness and width, the result of binary operators are still well-defined. ]
4 For every integer-class type
I
, letB(I)
be a unique hypothetical extended integer type of the same signedness with the same width (6.8.2 [basic.fundamental]) asI
the smallest width (6.8.2 [basic.fundamental]) capable of representing the same range of values. The width ofI
is equal to the width ofB(I)
. For every integral typeJ
, letB(J)
be the same type asJ
.[ Editor's note: Reorder paragraph 6 before 5. ]
6 Expressions of integer-class type are explicitly convertible to any integer-likeintegral type, and implicitly convertible to any integer-class type of equal or greater width and the same signedness. Expressions of integral type are both implicitly and explicitly convertible to any integer-class type. Conversions between integral and integer-class types and between two integer-class types do not exit via an exception. The result of such a conversion is the unique value of the destination type that is congruent to the source modulo 2N, where N is the width of the destination type.
5 Let
a
andb
be an objectobjects of integer-class typeI
, letb
be an object of integer-like typeI2
such that the expressionb
is implicitly convertible toI
, letx
andy
be respectively objects of typeB(I)
andB(I2)
as described above that represent the same values asa
andb
respectively, and letc
be an lvalue of any integral type.
- (5.1) For every unary operator
@
for which the expression@x
is well-formed,@a
shall also be well-formed and have the same value, effects, and value category as@x
provided that value is representable byI
. If@x
has typebool
, so too does@a
; if@x
has typeB(I)
, then@a
has typeI
.- (5.2) For every assignment operator
@=
for whichc @= x
is well-formed,c @= a
shall also be well-formed and shall have the same value and effects asc @= x
. The expressionc @= a
shall be an lvalue referring toc
.- (5.3) For every non-assignment binary operator
@
for whichx @ y
andy @ x
areis well-formed,a @ b
andb @ a
shall also be well-formed and shall have the same value, effects, and value category asx @ y
andy @ x
respectivelyprovided that value is representable byI
. Ifx @ y
ory @ x
has typebool
, so too doesa @ b
orb @ a
, respectively; ifx @ y
ory @ x
has typeB(I)
, thena @ b
orb @ a
, respectively, has typeI
; ifx @ y
ory @ x
has typeB(I2)
, thena @ b
orb @ a
, respectively, has typeI2
..- (5.4) For every assignment operator
@=
for whichx @= y
is well-formed,a @= b
shall also be well-formed and shall have the same value and effects asx @= y
. The expressiona @= b
shall be an lvalue referring toa
.7 An expression
E
of integer-class typeI
is contextually convertible tobool
as if bybool(E != I(0))
.8 All integer-class types model
regular
(18.6 [concepts.object]) andtotally_ordered
(18.5.4 [concept.totallyordered]).9 A value-initialized object of integer-class type has value 0.
[ Drafting note: There are some issues with the
numeric_limit
specialization:digits
is defined in terms ofradix
, so it doesn’t make sense to define the former but not the latter, and the definition ofdigits
is also incorrect for signed types. Instead of trying to fix this piecemeal and maintain an ever-growing list, we can simply specify this in terms ofB(I)
. ]10 For every (possibly cv-qualified) integer-class type
I
, letnumeric_limits<B(I)>
be a hypothetical specialization that meets the requirements fornumeric_limit
specializations for arithmetic types (17.3.5 [numeric.limits]).numeric_limits<I>
is specialized such that each static data memberm
has the same value asnumeric_limits<B(I)>::m
, and each static member functionf
returnsI(numeric_limits<B(I)>::f())
.:
- (10.1)
numeric_limits<I>::is_specialized
istrue
,- (10.2)
numeric_limits<I>::is_signed
istrue
if and only ifI
is a signed-integer-class type,- (10.3)
numeric_limits<I>::is_integer
istrue
,- (10.4)
numeric_limits<I>::is_exact
istrue
,- (10.5)
numeric_limits<I>::digits
is equal to the width of the integer-class type,- (10.6)
numeric_limits<I>::digits10
is equal tostatic_cast<int>(digits * log10(2))
, and- (10.7)
numeric_limits<I>::min()
andnumeric_limits<I>::max()
return the lowest and highest representable values ofI
, respectively, andnumeric_limits<I>::lowest()
returnsnumeric_limits<I>::min()
.? For any two integer-like types
I1
andI2
, at least one of which is an integer-class type,common_type_t<I1, I2>
denotes an integer-class type whose width is not less than that ofI1
orI2
. If bothI1
andI2
are signed-integer-like types, thencommon_type_t<I1, I2>
is also a signed-integer-like type.12
is-integer-like<I>
is true if and only if I is an integer-like type.is-signed-integer-like<I>
is true if and only if I is a signed-integer-like type.
template<not-same-as<subrange> R> requires borrowed_range<R> && convertible-to-non-slicing<iterator_t<R>, I> && convertible_to<sentinel_t<R>, S> constexpr subrange(R&& r) requires (!StoreSize || sized_range<R>);
6 Effects: Equivalent to:
2 The name
views::take
denotes a range adaptor object (24.7.2 [range.adaptor.object]). LetE
andF
be expressions, letT
beremove_cvref_t<decltype((E))>
, and letD
berange_difference_t<decltype((E))>
. Ifdecltype((F))
does not modelconvertible_to<D>
,views::take(E, F)
is ill-formed. Otherwise, the expressionviews::take(E, F)
is expression-equivalent to:
(2.1) If T is a specialization of
ranges::empty_view
(24.6.2.2 [range.empty.view]), then((void) F, decay-copy(E))
, except that the evaluations ofE
andF
are indeterminately sequenced.(2.2) Otherwise, if
T
modelsrandom_access_range
andsized_range
and is
- (2.2.1) […]
then
T(ranges::begin(E), ranges::begin(E) + std::min<D>(ranges::distancesize(E), F))
, except thatE
is evaluated only once.(2.3) Otherwise,
ranges::take_view(E, F)
.
namespace std::ranges { template<view V> class take_view : public view_interface<take_view<V>> { // [...] constexpr auto begin() requires (!simple-view<V>) { if constexpr (sized_range<V>) { if constexpr (random_access_range<V>) return ranges::begin(base_); else { auto sz = range_difference_t<V>(size()); return counted_iterator((ranges::begin(base_), sz); } } else return counted_iterator(ranges::begin(base_), count_); } constexpr auto begin() const requires range<const V> { if constexpr (sized_range<const V>) { if constexpr (random_access_range<const V>) return ranges::begin(base_); else { auto sz = range_difference_t<const V>(size()); return counted_iterator(ranges::begin(base_), sz); } } else return counted_iterator(ranges::begin(base_), count_); } constexpr auto end() requires (!simple-view<V>) { if constexpr (sized_range<V>) { if constexpr (random_access_range<V>) return ranges::begin(base_) + range_difference_t<V>(size()); else return default_sentinel; } else return sentinel<false>{ranges::end(base_)}; } constexpr auto end() const requires range<const V> { if constexpr (sized_range<const V>) { if constexpr (random_access_range<const V>) return ranges::begin(base_) + range_difference_t<const V>(size()); else return default_sentinel; } else return sentinel<true>{ranges::end(base_)}; } // [...] }; template<class R> take_view(R&&, range_difference_t<R>) -> take_view<views::all_t<R>>; }
2 The name
views::drop
denotes a range adaptor object (24.7.2 [range.adaptor.object]). LetE
andF
be expressions, letT
beremove_cvref_t<decltype((E))>
, and letD
berange_difference_t<decltype((E))>
. Ifdecltype((F))
does not modelconvertible_to<D>
,views::drop(E, F)
is ill-formed. Otherwise, the expressionviews::drop(E, F)
is expression-equivalent to:
(2.1) If T is a specialization of
ranges::empty_view
(24.6.2.2 [range.empty.view]), then((void) F, decay-copy(E))
, except that the evaluations ofE
andF
are indeterminately sequenced.(2.2) Otherwise, if
T
modelsrandom_access_range
andsized_range
and is
- (2.2.1) […]
then
T(ranges::begin(E) + std::min<D>(ranges::distancesize(E), F), ranges::end(E))
, except thatE
is evaluated only once.(2.3) Otherwise,
ranges::drop_view(E, F)
.
2 The name
views::counted
denotes a customization point object (16.3.3.3.6 [customization.point.object]). LetE
andF
be expressions, letT
bedecay_t<decltype((E))>
, and letD
beiter_difference_t<T>
. Ifdecltype((F))
does not modelconvertible_to<D>
,views::counted(E, F)
is ill-formed.[ Note 1: This case can result in substitution failure when
views::counted(E, F)
appears in the immediate context of a template instantiation. — end note ]Otherwise,
views::counted(E, F)
is expression-equivalent to:
- (2.1) If
T
modelscontiguous_iterator
, thenspan(to_address(E), static_cast<size_t>(static_cast<D>(F)))
.- (2.2) Otherwise, if
T
modelsrandom_access_iterator
, thensubrange(E, E + static_cast<D>(F))
, except thatE
is evaluated only once.- (2.3) Otherwise,
subrange(counted_iterator(E, F), default_sentinel)
.
? In the description of the algorithms, given an iterator
a
whose difference type isD
, and an expressionn
of integer-like type other thancv D
, the semantics ofa + n
anda - n
are, respectively, those ofa + D(n)
anda - D(n)
.
[LWG3366] Casey Carter. Narrowing conversions between integer and integer-class types.
https://wg21.link/lwg3366
[LWG3376] Jonathan Wakely. “integer-like class type” is too restrictive.
https://wg21.link/lwg3376
[N4885] Thomas Köppe. 2021-03-17. Working Draft, Standard for Programming Language C++.
https://wg21.link/n4885
[P1522R1] Eric Niebler. 2019-07-28. Iterator Difference Type and Integer Overflow.
https://wg21.link/p1522r1
[P2321R1] Tim Song. 2021-04-11. zip.
https://wg21.link/p2321r1
[P2367R0] Tim Song. 2021-04-30. Remove misuses of list-initialization from Clause 24.
https://wg21.link/p2367r0