Revised 2026-02-04 at 12:39:41 UTC

Tentative Issues


3831(i). Two-digit formatting of negative year is ambiguous

Section: 30.12 [time.format], 30.13 [time.parse] Status: Tentatively Ready Submitter: Matt Stephanson Opened: 2022-11-18 Last modified: 2025-12-04

Priority: 3

View other active issues in [time.format].

View all other issues in [time.format].

View all issues with Tentatively Ready status.

Discussion:

An issue has been identified regarding the two-digit formatting of negative years according to Table [tab:time.format.spec] (30.12 [time.format]):

cout << format("{:%y} ", 1976y)  // "76"
     << format("{:%y}", -1976y); // also "76"?

The relevant wording is

The last two decimal digits of the year. If the result is a single digit it is prefixed by 0. The modified command %Oy produces the locale's alternative representation. The modified command %Ey produces the locale's alternative representation of offset from %EC (year only).

MSVC STL treats the regular modified form symmetrically. Just as %Ey is the offset from %EC, so %y is the offset from %C, which is itself "[t]he year divided by 100 using floored division." (emphasis added). Because -1976 is the 24th year of the -20th century, the above code will print "76 24" using MSVC STL. However, many users expect, and libc++ gives, a result based on the literal wording, "76 76".

IEEE 1003.1-2008 strftime expects the century to be nonnegative, but the glibc implementation prints 24 for -1976. My own opinion is that this is the better result, because it consistently interprets %C and %y as the quotient and remainder of floored division by 100.

Howard Hinnant, coauthor of the original 30.12 [time.format] wording in P0355 adds:

On the motivation for this design it is important to remember a few things:

This leaves how to represent negative years with %y. I can think of 3 options:

  1. Use the last two digits without negating: -1976 → 76.

  2. Use the last two digits and negate it: -1976 → -76.

  3. Use floored modulus arithmetic: -1976 → 24.

The algorithm to convert %C and %y into a year is not important to the client because these are both strings, not integers. The client will do it with parse, not 100*C + y.

I discounted solution 3 as not sufficiently obvious. If the output for -1976 was 23, the human reader wouldn't immediately know that this is off by 1. The reader is expecting the POSIX spec:

the last two digits of the year as a decimal number [00,99].

24 just doesn't cut it.

That leaves solution 1 or 2. I discounted solution 2 because having the negative in 2 places (the %C and %y) seemed overly complicated and more error prone. The negative sign need only be in one place, and it has to be in %C to prevent ambiguity.

That leaves solution 1. I believe this is the solution for an extension of the POSIX spec to negative years with the property of least surprise to the client. The only surprise is in %C, not %y, and the surprise in %C seems unavoidable.

[2022-11-30; Reflector poll]

Set priority to 3 after reflector poll.

A few votes for priority 2. Might need to go to LEWG.

Previous resolution [SUPERSEDED]:

This wording is relative to N4917.

[Drafting Note: Two mutually exclusive options are prepared, depicted below by Option A and Option B, respectively.]

Option A: This is Howard Hinnant's choice (3)

  1. Modify 30.12 [time.format], Table [tab:time.format.spec] as indicated:

    Table 102 — Meaning of conversion specifiers [tab:time.format.spec]
    Specifier Replacement
    […]
    %y The last two decimal digits of the yearremainder after dividing the year by 100 using floored division.
    If the result is a single digit it is prefixed by 0.
    The modified command %Oy produces the locale's alternative representation. The
    modified command %Ey produces the locale's alternative representation of offset from
    %EC (year only).
    […]
  2. Modify 30.13 [time.parse], Table [tab:time.parse.spec] as indicated:

    Table 103 — Meaning of parse flags [tab:time.parse.spec]
    Flag Parsed value
    […]
    %y The last two decimal digits of the yearremainder after dividing the year by 100 using floored division.
    If the century is not otherwise specified (e.g.
    with %C), values in the range [69, 99] are presumed to refer to the years 1969 to 1999,
    and values in the range [00, 68] are presumed to refer to the years 2000 to 2068. The
    modified command %N y specifies the maximum number of characters to read. If N is
    not specified, the default is 2. Leading zeroes are permitted but not required. The
    modified commands %Ey and %Oy interpret the locale's alternative representation.
    […]

Option B: This is Howard Hinnant's choice (1)

  1. Modify 30.12 [time.format], Table [tab:time.format.spec] as indicated:

    Table 102 — Meaning of conversion specifiers [tab:time.format.spec]
    Specifier Replacement
    […]
    %y The last two decimal digits of the year, regardless of the sign of the year.
    If the result is a single digit it is prefixed by 0.
    The modified command %Oy produces the locale's alternative representation. The
    modified command %Ey produces the locale's alternative representation of offset from
    %EC (year only).
    [Example ?: cout << format("{:%C %y}", -1976y); prints -20 76. — end example]
    […]
  2. Modify 30.13 [time.parse], Table [tab:time.parse.spec] as indicated:

    Table 103 — Meaning of parse flags [tab:time.parse.spec]
    Flag Parsed value
    […]
    %y The last two decimal digits of the year, regardless of the sign of the year.
    If the century is not otherwise specified (e.g.
    with %C), values in the range [69, 99] are presumed to refer to the years 1969 to 1999,
    and values in the range [00, 68] are presumed to refer to the years 2000 to 2068. The
    modified command %N y specifies the maximum number of characters to read. If N is
    not specified, the default is 2. Leading zeroes are permitted but not required. The
    modified commands %Ey and %Oy interpret the locale's alternative representation.
    [Example ?: year y; istringstream{"-20 76"} >> parse("%3C %y", y); results in
    y == -1976y. — end example]
    […]

[2025-10-17; Jonathan provides updated wording using Option B]

[2025-12-04; Reflector poll.]

Set status to Tentatively Ready after seven votes in favour during reflector poll.

Proposed resolution:

This wording is relative to N5014.

  1. Modify 30.12 [time.format], Table [tab:time.format.spec] as indicated:

    Table 133 — Meaning of conversion specifiers [tab:time.format.spec]
    Specifier Replacement
    […]
    %y The last two decimal digits of the year, regardless of the sign of the year.
    If the result is a single digit it is prefixed by 0.
    The modified command %Oy produces the locale's alternative representation. The
    modified command %Ey produces the locale's alternative representation of offset from
    %EC (year only).
    [Example ?: cout << format("{:%C %y}", -1976y); prints -20 76. — end example]
    […]
  2. Modify 30.13 [time.parse], Table [tab:time.parse.spec] as indicated:

    Table 103 — Meaning of parse flags [tab:time.parse.spec]
    Flag Parsed value
    […]
    %y The last two decimal digits of the year, regardless of the sign of the year.
    If the century is not otherwise specified (e.g.
    with %C), values in the range [69, 99] are presumed to refer to the years 1969 to 1999,
    and values in the range [00, 68] are presumed to refer to the years 2000 to 2068. The
    modified command %N y specifies the maximum number of characters to read. If N is
    not specified, the default is 2. Leading zeroes are permitted but not required. The
    modified commands %Ey and %Oy interpret the locale's alternative representation.
    [Example ?: year y; istringstream{"-20 76"} >> parse("%3C %y", y); results in
    y == -1976y. — end example]
    […]

4090(i). Underspecified use of locale facets for locale-dependent std::format

Section: 28.5.2.2 [format.string.std] Status: Tentatively Ready Submitter: Jens Maurer Opened: 2024-04-30 Last modified: 2026-01-19

Priority: 3

View other active issues in [format.string.std].

View all other issues in [format.string.std].

View all issues with Tentatively Ready status.

Discussion:

There are std::format variants that take an explicit std::locale parameter. There is the "L" format specifier that uses that locale (or some environment locale) for formatting, according to 28.5.2.2 [format.string.std] p17:

"For integral types, the locale-specific form causes the context's locale to be used to insert the appropriate digit group separator characters."

It is unclear which specific facets are used to make this happen. This is important, because users can install their own facets into a given locale. Specific questions include:

Assuming the encoding for char is UTF-8, the use of a user-provided num_put<> facet (as opposed to std::format creating the output based on numpunct<>) would allow digit separators that are not expressibly as a single UTF-8 code unit.

[2024-05-08; Reflector poll]

Set priority to 3 after reflector poll.

[2024-06-12; SG16 meeting]

The three major implementations all use numpunct but not num_put, clarify that this is the intended behaviour.

[2025-06-12; Jonathan provides wording]

Previous resolution [SUPERSEDED]:

This wording is relative to N5008.

  1. Modify 28.5.2.2 [format.string.std] as indicated:

    -17- When the L option is used, the form used for the conversion is called the locale-specific form. The L option is only valid for arithmetic types, and its effect depends upon the type.
    1. (17.1) — For integral types, the locale-specific form causes the context’s locale to be used to insert the appropriate digit group separator characters as if obtained with numpunct<charT>::grouping and numpunct<charT>::thousands_sep .
    2. (17.2) — For floating-point types, the locale-specific form causes the context’s locale to be used to insert the appropriate digit group and radix separator characters as if obtained with numpunct<charT>::grouping, numpunct<charT>::thousands_sep, and numpunct<charT>::decimal_point .
    3. (17.3) — For the textual representation of bool, the locale-specific form causes the context’s locale to be used to insert the appropriate string as if obtained with numpunct<charT>::truename or numpunct<charT>::falsename.

[2025-08-27; SG16 meeting]

SG16 unanimously approved new wording from Victor. The new wording incorporates similar wording as added by P2419R2 to address 3565(i). Status updated SG16 → Open.

[2026-01-16; Reflector poll.]

Set status to Tentatively Ready after six votes in favour during reflector poll.

Proposed resolution:

This wording is relative to N5014.

  1. Modify 28.5.2.2 [format.string.std] as indicated:

    -17- When the L option is used, the form used for the conversion is called the locale-specific form. The L option is only valid for arithmetic types, and its effect depends upon the type.
    1. (17.1) — For integral types, the locale-specific form causes the context’s locale to be used to insert the appropriate digit group separator characters as if obtained with numpunct<charT>::grouping and numpunct<charT>::thousands_sep .
    2. (17.2) — For floating-point types, the locale-specific form causes the context’s locale to be used to insert the appropriate digit group and radix separator characters as if obtained with numpunct<charT>::grouping, numpunct<charT>::thousands_sep, and numpunct<charT>::decimal_point .
    3. (17.3) — For the textual representation of bool, the locale-specific form causes the context’s locale to be used to insert the appropriate string as if obtained with numpunct<charT>::truename or numpunct<charT>::falsename.
    If the string literal encoding is a Unicode encoding form and the locale is among an implementation-defined set of locales, each replacement that depends on the locale is performed as if the replacement character sequence is converted to the string literal encoding.

4259(i). P1148R0 changed the return values of searching functions of std::basic_string on some platforms

Section: 27.4.3.8.2 [string.find] Status: Tentatively Ready Submitter: Jiang An Opened: 2025-05-05 Last modified: 2025-12-04

Priority: 3

View other active issues in [string.find].

View all other issues in [string.find].

View all issues with Tentatively Ready status.

Discussion:

P1148R0 respecified the searching functions of std::basic_string to return corresponding string view type's npos member constant (equal to std::size_t(-1)), converted to the string type S's member S::size_type, when the search fails. Before the change, S::npos (equal to S::size_type(-1)) was returned on failure.

On platforms where std::size_t isn't the widest unsigned integer type (e.g. on usual 32-bit platforms), the return value can change. Because there can be an allocator with a wider size_type, and when the basic_string type S uses such an allocator, S::size_type is specified to be that type, which in turn makes S::size_type(std::size_t(-1)) not equal to S::size_type(-1).

Do we want to restore the old return values?

Previous resolution [SUPERSEDED]:

This wording is relative to N5008.

  1. Modify 27.4.3.8.2 [string.find] as indicated:

    template<class T>
      constexpr size_type find(const T& t, size_type pos = 0) const noexcept(see below);
    […]
    template<class T>
      constexpr size_type find_last_not_of(const T& t, size_type pos = npos) const noexcept(see below);
    

    -2- Constraints: […]

    -3- Effects: Let G be the name of the function. Equivalent to:

    basic_string_view<charT, traits> s = *this, sv = t;
    return s.G(sv, pos);
    if (auto result = s.G(sv, pos); result == size_t(-1))
      return npos;
    else
      return result;
    

[2025-06-10, reflector discussion]

During reflector discussion of this issue there was a preference to adjust the proposed wording to use s.npos instead of size_t(-1).

[2025-10-21; Reflector poll.]

Set priority to 3 after reflector poll.

[2025-12-04; Reflector poll.]

Set status to Tentatively Ready after five votes in favour during reflector poll.

Proposed resolution:

This wording is relative to N5008.

  1. Modify 27.4.3.8.2 [string.find] as indicated:

    template<class T>
      constexpr size_type find(const T& t, size_type pos = 0) const noexcept(see below);
    […]
    template<class T>
      constexpr size_type find_last_not_of(const T& t, size_type pos = npos) const noexcept(see below);
    

    -2- Constraints: […]

    -3- Effects: Let G be the name of the function. Equivalent to:

    basic_string_view<charT, traits> s = *this, sv = t;
    return s.G(sv, pos);
    if (auto result = s.G(sv, pos); result == s.npos)
      return npos;
    else
      return result;
    

4324(i). unique_ptr<void>::operator* is not SFINAE-friendly

Section: 20.3.1.3.5 [unique.ptr.single.observers] Status: Tentatively Ready Submitter: Hewill Kang Opened: 2025-08-24 Last modified: 2025-12-04

Priority: 3

View other active issues in [unique.ptr.single.observers].

View all other issues in [unique.ptr.single.observers].

View all issues with Tentatively Ready status.

Discussion:

LWG 2762(i) added a conditional noexcept specification to unique_ptr::operator* to make it consistent with shared_ptr::operator*:

constexpr add_lvalue_reference_t<T> operator*() const noexcept(noexcept(*declval<pointer>()));

This unexpectedly makes unique_ptr<void>::operator* no longer SFINAE-friendly, for example:

#include <memory>

template<class T> concept dereferenceable = requires(T& t) { *t; };

static_assert( dereferenceable<int *>);
static_assert(!dereferenceable<void*>);

static_assert( dereferenceable<std::shared_ptr<int >>);
static_assert(!dereferenceable<std::shared_ptr<void>>);

static_assert( dereferenceable<std::unique_ptr<int >>);
static_assert( dereferenceable<std::unique_ptr<void>>); // hard error

Given that the standard intends for operator* of shared_ptr and unique_ptr to be SFINAE-friendly based on 20.3.2.2.6 [util.smartptr.shared.obs], regardless of the value of static_assert, it is reasonable to assume that there should be no hard error here.

Previous resolution [SUPERSEDED]:

This wording is relative to N5014.

  1. Modify 20.3.1.3.5 [unique.ptr.single.observers] as indicated:

    constexpr add_lvalue_reference_t<T> operator*() const noexcept(noexcept(*declval<pointer>())see below);
    

    -1- Mandates: reference_converts_from_temporary_v<add_lvalue_reference_t<T>, decltype(*declval<pointer>())> is false.

    -2- Preconditions: get() != nullptr is true.

    -3- Returns: *get().

    -?- Remarks:: The exception specification is equivalent to:

    !requires { *declval<pointer>(); } || requires { { *declval<pointer>() } noexcept; }
    

[2025-08-26; Reflector discussion]

During reflector triaging it had been pointed out that a better solution would be to constrain the operator* directly. The proposed wording has been updated to that effect.

[2025-10-22; Reflector poll.]

Set priority to 3 after reflector poll.

[2025-12-04; Reflector poll.]

Set status to Tentatively Ready after eight votes in favour during reflector poll.

Proposed resolution:

This wording is relative to N5014.

  1. Modify 20.3.1.3.5 [unique.ptr.single.observers] as indicated:

    constexpr add_lvalue_reference_t<T> operator*() const noexcept(noexcept(*declval<pointer>()));
    

    -?- Constraints: *declval<pointer>() is a well-formed expression.

    -1- Mandates: reference_converts_from_temporary_v<add_lvalue_reference_t<T>, decltype(*declval<pointer>())> is false.

    -2- Preconditions: get() != nullptr is true.

    -3- Returns: *get().


4417(i). views::indices is underconstrained

Section: 25.6.4.1 [range.iota.overview] Status: Tentatively NAD Submitter: Hewill Kang Opened: 2025-10-15 Last modified: 2026-01-16

Priority: Not Prioritized

View all other issues in [range.iota.overview].

View all issues with Tentatively NAD status.

Discussion:

Whether an integer-class type satisfies weakly_incrementable is unspecified according to 25.6.4.2 [range.iota.view]. For example, the library may provide a member type alias difference_type for the integer-class type to make it weakly_incrementable, or not.

If the latter, then views::iota(integer-class-type(0)) is always ill-formed because iota_view<W, Bound> requires W to be weakly_incrementable.

However, unlike views::iota, views::indices unconditionally accepts integer-class types, which will lead to a hard error in the function body if the integer-class type is not weakly_incrementable, which is true for both libstdc++ and MSVC-STL.

[2026-01-16; Reflector poll. Status → Tentatively NAD.]

"This is implied by the expression-equivalence."

Proposed resolution:

This wording is relative to N5014.

  1. Modify 25.6.4.1 [range.iota.overview] as indicated:

    -4- The name views::indices denotes a customization point object (16.3.3.3.5 [customization.point.object]). Given subexpression E, let T be remove_cvref_t<decltype((E))>. views::indices(E) is expression-equivalent to views::iota(T(0), E) if is-integer-like<T> is true and T models weakly_incrementable, and ill-formed otherwise.


4457(i). freestanding for stable_sort, stable_partition and inplace_merge

Section: 26.4 [algorithm.syn] Status: Tentatively Ready Submitter: Braden Ganetsky Opened: 2025-11-06 Last modified: 2026-01-16

Priority: Not Prioritized

View all other issues in [algorithm.syn].

View all issues with Tentatively Ready status.

Discussion:

Addresses US 157-255

This applies the resolution for US 157-255.

[2026-01-16; Reflector poll.]

Set status to Tentatively Ready after ten votes in favour during reflector poll.

Proposed resolution:

This wording is relative to N5014.

  1. Modify 26.4 [algorithm.syn], as indicated:

    namespace ranges {
      template<random_access_iterator I, sentinel_for<I> S, class Comp = ranges::less,
               class Proj = identity>
        requires sortable<I, Comp, Proj>
        constexpr I stable_sort(I first, S last, Comp comp = {}, Proj proj = {});         // hosted
      template<random_access_range R, class Comp = ranges::less, class Proj = identity>
        requires sortable<iterator_t<R>, Comp, Proj>
        constexpr borrowed_iterator_t<R>
          stable_sort(R&& r, Comp comp = {}, Proj proj = {});                             // hosted
    
      template<execution-policy Ep, random_access_iterator I, sized_sentinel_for<I> S,
               class Comp = ranges::less, class Proj = identity>
        requires sortable<I, Comp, Proj>
        I stable_sort(Ep&& exec, I first, S last, Comp comp = {},
                      Proj proj = {});                                        // freestanding-deletedhosted
      template<execution-policy Ep, sized-random-access-range R, class Comp = ranges::less,
               class Proj = identity>
        requires sortable<iterator_t<R>, Comp, Proj>
        borrowed_iterator_t<R>
          stable_sort(Ep&& exec, R&& r, Comp comp = {}, Proj proj = {});      // freestanding-deletedhosted
    }
    
  2. Modify 26.4 [algorithm.syn], as indicated:

    namespace ranges {
      template<bidirectional_iterator I, sentinel_for<I> S, class Proj = identity,
               indirect_unary_predicate<projected<I, Proj>> Pred>
        requires permutable<I>
        constexpr subrange<I> stable_partition(I first, S last, Pred pred,            // hosted
                                               Proj proj = {});
      template<bidirectional_range R, class Proj = identity,
               indirect_unary_predicate<projected<iterator_t<R>, Proj>> Pred>
        requires permutable<iterator_t<R>>
        constexpr borrowed_subrange_t<R> stable_partition(R&& r, Pred pred,           // hosted
                                                          Proj proj = {});
    
      template<execution-policy Ep, random_access_iterator I, sized_sentinel_for<I> S,
               class Proj = identity, indirect_unary_predicate<projected<I, Proj>> Pred>
        requires permutable<I>
        subrange<I>
          stable_partition(Ep&& exec, I first, S last, Pred pred,
                           Proj proj = {});                                   // freestanding-deletedhosted
      template<execution-policy Ep, sized-random-access-range R, class Proj = identity,
               indirect_unary_predicate<projected<iterator_t<R>, Proj>> Pred>
        requires permutable<iterator_t<R>>
        borrowed_subrange_t<R>
          stable_partition(Ep&& exec, R&& r, Pred pred, Proj proj = {});      // freestanding-deletedhosted
    }
    
  3. Modify 26.4 [algorithm.syn], as indicated:

    namespace ranges {
      template<bidirectional_iterator I, sentinel_for<I> S, class Comp = ranges::less,
               class Proj = identity>
        requires sortable<I, Comp, Proj>
        constexpr I
          inplace_merge(I first, I middle, S last, Comp comp = {}, Proj proj = {});       // hosted
      template<bidirectional_range R, class Comp = ranges::less, class Proj = identity>
        requires sortable<iterator_t<R>, Comp, Proj>
        constexpr borrowed_iterator_t<R>
          inplace_merge(R&& r, iterator_t<R> middle, Comp comp = {}, Proj proj = {});     // hosted
    
      template<execution-policy Ep, random_access_iterator I, sized_sentinel_for<I> S,
               class Comp = ranges::less, class Proj = identity>
        requires sortable<I, Comp, Proj>
        I inplace_merge(Ep&& exec, I first, I middle, S last, Comp comp = {},
                        Proj proj = {});                          // freestanding-deletedhosted
      template<execution-policy Ep, sized-random-access-range R, class Comp = ranges::less,
               class Proj = identity>
        requires sortable<iterator_t<R>, Comp, Proj>
        borrowed_iterator_t<R>
          inplace_merge(Ep&& exec, R&& r, iterator_t<R> middle, Comp comp = {},
                        Proj proj = {});                          // freestanding-deletedhosted
    }
    

4460(i). Missing Throws: for last variant constructor

Section: 22.6.3.2 [variant.ctor] Status: Tentatively Ready Submitter: Jonathan Wakely Opened: 2025-11-07 Last modified: 2025-12-04

Priority: Not Prioritized

View other active issues in [variant.ctor].

View all other issues in [variant.ctor].

View all issues with Tentatively Ready status.

Discussion:

All variant constructors except the last one have a Throws: element saying what they're allowed to throw.

This originates from an editorial pull request, where the submitter said:

"It looks like this defect is an artifact of a change between P0088R0 and P0088R1. Note how in R0 neither one of the emplaced_type_t/emplaced_index_t (as they were then called) + initializer_list constructors have a throws clause. In R1 only one of them gained it."

Previous resolution [SUPERSEDED]:

This wording is relative to N5014.

  1. Modify 22.6.3.2 [variant.ctor], as indicated:

    template<size_t I, class U, class... Args>
      constexpr explicit variant(in_place_index_t<I>, initializer_list<U> il, Args&&... args);
    

    -35- Constraints:

    1. (35.1) — I is less than sizeof...(Types) and
    2. (35.2) — is_constructible_v<TI, initializer_list<U>&, Args...> is true.

    -36- Effects: Direct-non-list-initializes the contained value of type TI with il, std::forward<Args>(args)....

    -37- Postconditions: index() is I.

    -?- Throws: Any exception thrown by calling the selected constructor of TI.

    -38- Remarks: If TI’s selected constructor is a constexpr constructor, this constructor is a constexpr constructor.

[2025-11-11; Jonathan provides improved wording]

[2025-12-04; Reflector poll.]

Set status to Tentatively Ready after six votes in favour during reflector poll.

Proposed resolution:

This wording is relative to N5014.

  1. Modify 22.6.3.2 [variant.ctor], as indicated:

    constexpr variant() noexcept(see below);
    

    -2- Constraints: is_default_constructible_v<T0> is true.

    -3- Effects: Constructs a variant holding a value-initialized value of type T0.

    -4- Postconditions: valueless_by_exception() is false and index() is 0.

    -5- Throws: Any exception thrown by the value-initialization of T0.

    -6- Remarks: […]

    constexpr variant(const variant&);
    

    -7- Effects: If w holds a value, initializes the variant to hold the same alternative as w and direct-initializes the contained value with GET<j>(w), where j is w.index(). Otherwise, initializes the variant to not hold a value.

    -8- Throws: Any exception thrown by direct-initializating any Ti for all i the initialization of the contained value.

    -9- Remarks: […]

    constexpr variant(variant&&) noexcept(see below);
    

    -10- Constraints: is_move_constructible_v<Ti> is true for all i.

    -11- Effects: If w holds a value, initializes the variant to hold the same alternative as w and direct-initializes the contained value with GET<j>(std::move(w)), where j is w.index(). Otherwise, initializes the variant to not hold a value.

    -12- Throws: Any exception thrown by move-constructing any Ti for all i the initialization of the contained value.

    -13- Remarks: […]

    template<class T> constexpr variant(T&&) noexcept(see below);
    

    -14- Let Tj be a type that is determined as follows: build an imaginary function FUN(Ti) for each alternative type Ti for which Ti x[] = {std::forward<T>(t)}; is well-formed for some invented variable x. The overload FUN(Tj) selected by overload resolution for the expression FUN(std::forward<T>(t)) defines the alternative Tj which is the type of the contained value after construction.

    -15- Constraints: […]

    -16- Effects: Initializes *this to hold the alternative type Tj and direct-non-list-initializes the contained value with std::forward<T>(t).

    -17- Postconditions: […]

    -18- Throws: Any exception thrown by the initialization of the selected alternative Tj contained value.

    -19- Remarks: […]

    template<class T, class... Args> constexpr variant(in_place_type_t<T>, Args&&... args);
    

    -20- Constraints: […]

    -21- Effects: Direct-non-list-initializes the contained value of type T with std::forward<Args>(args)....

    -22- Postconditions: […]

    -23- Throws: Any exception thrown by the selected constructor of T the initialization of the contained value.

    -24- Remarks: […]

    template<class T, class U, class... Args>
      constexpr variant(in_place_type_t<T>, initializer_list<U> li, Args&&... args);
    

    -25- Constraints: […]

    -26- Effects: Direct-non-list-initializes the contained value of type T with il, std::forward<Args>(args)....

    -27- Postconditions: […]

    -28- Throws: Any exception thrown by the selected constructor of T the initialization of the contained value.

    -29- Remarks: […]

    template<size_t I, class... Args>
      constexpr explicit variant(in_place_index_t<I>, Args&&... args);
    

    -30- Constraints:

    1. (30.1) — I is less than sizeof...(Types) and
    2. (30.2) — is_constructible_v<TI, Args...> is true.

    -31- Effects: Direct-non-list-initializes the contained value of type TI with std::forward<Args>(args)....

    -32- Postconditions: index() is I.

    -33- Throws: Any exception thrown by the selected constructor of Ti the initialization of the contained value.

    -34- Remarks: If TI’s selected constructor is a constexpr constructor, this constructor is a constexpr constructor.

    template<size_t I, class U, class... Args>
      constexpr explicit variant(in_place_index_t<I>, initializer_list<U> il, Args&&... args);
    

    -35- Constraints:

    1. (35.1) — I is less than sizeof...(Types) and
    2. (35.2) — is_constructible_v<TI, initializer_list<U>&, Args...> is true.

    -36- Effects: Direct-non-list-initializes the contained value of type TI with il, std::forward<Args>(args)....

    -37- Postconditions: index() is I.

    -?- Throws: Any exception thrown by the initialization of the contained value.

    -38- Remarks: If TI’s selected constructor is a constexpr constructor, this constructor is a constexpr constructor.


4467(i). hive::splice can throw bad_alloc

Section: 23.3.9.5 [hive.operations] Status: Tentatively Ready Submitter: Jonathan Wakely Opened: 2025-11-07 Last modified: 2025-12-04

Priority: Not Prioritized

View other active issues in [hive.operations].

View all other issues in [hive.operations].

View all issues with Tentatively Ready status.

Discussion:

Moving blocks from the source hive to the destination hive might require reallocating the array of pointers to blocks, so the Throws: element should allow this.

[2025-12-04; Reflector poll.]

Set status to Tentatively Ready after seven votes in favour during reflector poll.

Proposed resolution:

This wording is relative to N5014.

  1. Modify 23.3.9.5 [hive.operations], as indicated:

    void splice(hive& x);
    void splice(hive&& x);
    

    -2- Preconditions: get_allocator() == x.get_allocator() is true.

    -3- Effects: If addressof(x) == this is true, the behavior is erroneous and there are no effects. Otherwise, inserts the contents of x into *this and x becomes empty. Pointers and references to the moved elements of x now refer to those same elements but as members of *this. Iterators referring to the moved elements continue to refer to their elements, but they now behave as iterators into *this, not into x.

    -4- Throws: length_error if any of x's active blocks are not within the bounds of current-limits , as well as any exceptions thrown by the allocator.

    -5- Complexity: Linear in the sum of all element blocks in x plus all element blocks in *this.

    -6- Remarks: Reserved blocks in x are not transferred into *this. If addressof(x) == this is false, invalidates the past-the-end iterator for both x and *this.


4468(i). §[const.wrap.class] "operator decltype(auto)" is ill-formed

Section: 21.3.5 [const.wrap.class] Status: Tentatively Ready Submitter: Jiang An Opened: 2025-11-07 Last modified: 2025-12-04

Priority: Not Prioritized

View other active issues in [const.wrap.class].

View all other issues in [const.wrap.class].

View all issues with Tentatively Ready status.

Discussion:

Following the approval of CWG 1670 in Kona 2025, the following declaration in class template constant_wrapper, 21.3.5 [const.wrap.class], is ill-formed:

constexpr operator decltype(auto)() const noexcept { return value; }

[2025-12-04; Reflector poll.]

Set status to Tentatively Ready after seven votes in favour during reflector poll.

Proposed resolution:

This wording is relative to N5014.

  1. Modify 21.3.5 [const.wrap.class], class template constant_wrapper synopsis, as indicated:

    […]
    template<cw-fixed-value X, class>
    struct constant_wrapper : cw-operators {
      static constexpr const auto & value = X.data;
    
      using type = constant_wrapper;
      using value_type = typename decltype(X)::type;
    
      […]
      constexpr operator decltype(autovalue)() const noexcept { return value; }
    };
    

4474(i). "round_to_nearest" rounding mode is unclear

Section: 17.3.4 [round.style] Status: Tentatively Ready Submitter: Jan Schultke Opened: 2025-11-13 Last modified: 2026-01-16

Priority: Not Prioritized

View all issues with Tentatively Ready status.

Discussion:

Consider the specification of round_to_nearest in 17.3.4 [round.style]:

round_to_nearest if the rounding style is to the nearest representable value

It is unclear how exact ties are rounded. For example, with this rounding, would a value that is equidistant to zero and numeric_limits<float>::min() be rounded towards zero or away from zero?

In 17.3.5.2 [numeric.limits.members], there exists a footnote for numeric_limits<T>::round_style:

185) Equivalent to FLT_ROUNDS. Required by ISO/IEC 10967-1:2012.

In C23 5.2.4.2.2 [Characteristics of floating types <float.h>], it is specified that a value of 1 for FLT_ROUNDS (which equals round_to_nearest) means

to nearest, ties to even

This is also the default ISO/IEC 60559 rounding mode, and chosen by standard libraries such as libstdc++ for numeric_limits under that assumption. Do note that C23 no longer references ISO/IEC 10967 in any normative wording, so presumably, matching FLT_ROUNDS values means to match the value that exists in C23, including its meaning.

[2026-01-16; Reflector poll.]

Set status to Tentatively Ready after seven votes in favour during reflector poll.

"IEEE 754-2019 talks about what to do in case of a tie for single digit precision types, such as FP4 (E2M1). Will raise this with WG14." [E2M1 is 1 sign bit, 2 exponent bits, 1 explicit mantissa bit]

Proposed resolution:

This wording is relative to N5014.

  1. Modify 17.3.4 [round.style] as indicated:

    -1- The rounding mode for floating-point arithmetic is characterized by the values:

    • (1.1) — round_indeterminate if the rounding style is indeterminable
    • (1.2) — round_toward_zero if the rounding style is toward zero
    • (1.3) — round_to_nearest if the rounding style is to the nearest representable value; if there are two equally near such values, the one with an even least significant digit is chosen
    • (1.4) — round_toward_infinity if the rounding style is toward infinity
    • (1.5) — round_toward_neg_infinity if the rounding style is toward negative infinity

4475(i). The description to single total order in [thread.mutex.requirements.mutex.general] is hollow

Section: 32.6.4.2.1 [thread.mutex.requirements.mutex.general] Status: Tentatively NAD Submitter: jim x Opened: 2025-11-14 Last modified: 2026-01-16

Priority: Not Prioritized

View all issues with Tentatively NAD status.

Discussion:

32.6.4.2.1 [thread.mutex.requirements.mutex.general] p4 says:

For purposes of determining the existence of a data race, these behave as atomic operations (6.10.2 [intro.multithread]). The lock and unlock operations on a single mutex appears to occur in a single total order.

Even for atomic operations, the precondition for ordering them in a single total order is that they must be memory_order::seq_cst operations, such that we can form the total order to reason.

Put aside the fact that we impose the preconditions on unlock and lock. Is this a possible total order if lock reads unlock_1, but there is a unlock_2 between them

unlock_1 < unlock_2 < lock

First, although we have said that lock and unlock operations behave as atomic operations, and lock reads unlock_1, meaning that unlock_1 is coherence-ordered before lock, however, we don't specify that they are memory_order::seq_cst operations, so 32.5.4 [atomics.order] p4 doesn't apply here

Second, for every pair of atomic operations A and B on an object M, where A is coherence-ordered before B, the following four conditions are required to be satisfied by S:

So, it is not helpful to decide that unlock_1 precedes lock in a single total order. Similarly, excluding unlock_1 < unlock_2 < lock is not possible.

Suggested resolution:

The lock and unlock operations on a single mutex appears to occur in a single total order; for this purpose, these operations are considered as memory_order::seq_cst operations

[2026-01-16; Reflector poll. Status → Tentatively NAD.]

For atomic objects, the modification order is already a single total order, seq_cst or not. This isn't a useful change.

Proposed resolution:

This wording is relative to N5014.

  1. Modify 32.6.4.2.1 [thread.mutex.requirements.mutex.general] as indicated:

    -4- The implementation provides lock and unlock operations, as described below. For purposes of determining the existence of a data race, these behave as atomic operations (6.10.2 [intro.multithread]). The lock and unlock operations on a single mutex appears to occur in a single total order; for this purpose, these operations are considered as memory_order::seq_cst operations.


4477(i). Placement operator delete should be constexpr

Section: 17.6.3.4 [new.delete.placement] Status: Tentatively Ready Submitter: Jakub Jelinek Opened: 2025-11-18 Last modified: 2025-11-26

Priority: Not Prioritized

View other active issues in [new.delete.placement].

View all other issues in [new.delete.placement].

View all issues with Tentatively Ready status.

Discussion:

The P2747R2 paper made placement operator new constexpr. At that time constexpr exceptions weren't in C++26, so that was all that was needed. But later on when P3068R5 was voted in, the P2747R2 changes look insufficient. The problem is that when you throw from a constructor during operator new, it invokes placement operator delete. And P2747R2 didn't touch that.

This makes it impossible to handle an exception thrown by a placement new-expression during constant evaluation.

[2025-11-26; Reflector poll.]

Set status to Tentatively Ready after six votes in favour during reflector poll.

Proposed resolution:

This wording is relative to N5014.

  1. Modify 17.6.2 [new.syn] as indicated:

    constexpr void* operator new  (std::size_t size, void* ptr) noexcept;
    constexpr void* operator new[](std::size_t size, void* ptr) noexcept;
    constexpr void operator delete  (void* ptr, void*) noexcept;
    constexpr void operator delete[](void* ptr, void*) noexcept;
    
  2. Modify 17.6.3.4 [new.delete.placement] as indicated:

    constexpr void* operator new(std::size_t size, void* ptr) noexcept;
    

    […]

    constexpr void* operator new[](std::size_t size, void* ptr) noexcept;
    

    […]

    constexpr void operator delete(void* ptr, void*) noexcept;
    

    -7- Effects: Intentionally performs no action.

    -8- Remarks: Default function called when any part of the initialization in a placement new-expression that invokes the library's non-array placement operator new terminates by throwing an exception (7.6.2.8 [expr.new]).

    constexpr void operator delete[](void* ptr, void*) noexcept;
    

    -9- Effects: Intentionally performs no action.

    -10- Remarks: Default function called when any part of the initialization in a placement new-expression that invokes the library's array placement operator new terminates by throwing an exception (7.6.2.8 [expr.new]).


4480(i). <stdatomic.h> should provide ATOMIC_CHAR8_T_LOCK_FREE

Section: 32.5.12 [stdatomic.h.syn] Status: Tentatively Ready Submitter: Jiang An Opened: 2025-11-21 Last modified: 2025-12-04

Priority: Not Prioritized

View all other issues in [stdatomic.h.syn].

View all issues with Tentatively Ready status.

Discussion:

Currently, <stdatomic.h> is specified to provide atomic_char8_t but not its corresponding ATOMIC_CHAR8_T_LOCK_FREE macro, which is self-inconsistent. Also, given WG14 N2653 added ATOMIC_CHAR8_T_LOCK_FREE to C's <stdatomic.h> in C23, perhaps C++ should do the same thing in the spirit of P3348R4.

[2025-12-04; Reflector poll.]

Set status to Tentatively Ready after seven votes in favour during reflector poll.

Proposed resolution:

This wording is relative to N5014.

  1. Modify 32.5.12 [stdatomic.h.syn], header <stdatomic.h> synopsis, as indicated:

    template<class T>
      using std-atomic = std::atomic<T>; // exposition only
    
    #define _Atomic(T) std-atomic<T>
    
    #define ATOMIC_BOOL_LOCK_FREE see below
    #define ATOMIC_CHAR_LOCK_FREE see below
    #define ATOMIC_CHAR8_T_LOCK_FREE see below
    #define ATOMIC_CHAR16_T_LOCK_FREE see below
    #define ATOMIC_CHAR32_T_LOCK_FREE see below
    #define ATOMIC_WCHAR_T_LOCK_FREE see below
    #define ATOMIC_SHORT_LOCK_FREE see below
    #define ATOMIC_INT_LOCK_FREE see below
    #define ATOMIC_LONG_LOCK_FREE see below
    #define ATOMIC_LLONG_LOCK_FREE see below
    #define ATOMIC_POINTER_LOCK_FREE see below
    
    […]
    

4481(i). Disallow chrono::duration<const T, P>

Section: 30.5.1 [time.duration.general] Status: Tentatively Ready Submitter: Jonathan Wakely Opened: 2025-11-26 Last modified: 2026-01-16

Priority: Not Prioritized

View all issues with Tentatively Ready status.

Discussion:

Using a const type as the rep for a chrono::duration causes various problems but there seems to be no rule preventing it.

The non-const member operators that modify a duration don't work if the rep type is const, e.g. duration<const int>::operator++() is typically ill-formed (unless the implementation chooses to store remove_cv_t<rep> as the data member). hash<duration<const int>> uses hash<const int> which is not enabled, so you can't hash a duration with a const rep. Generic code that wants to perform arithmetic with the rep type would need to remember to consistently use remove_cv_t<rep> to work correctly with const types.

We should just disallow const rep types. If you want a non-modifiable duration, use const duration<R,P> not duration<const R, P>

[2026-01-16; Reflector poll.]

Set status to Tentatively Ready after ten votes in favour during reflector poll.

This loses support for duration<volatile T, P> but that seems OK.

Proposed resolution:

This wording is relative to N5014.

  1. Modify 30.5.1 [time.duration.general] as indicated:

    -2- Rep shall be an arithmetic type or a class emulating an arithmetic type. If a specialization of duration is instantiated with a cv-qualified type or a specialization of duration type as the argument for the template parameter Rep, the program is ill-formed.

    -3- If Period is not a specialization of ratio, the program is ill-formed. If Period::num is not positive, the program is ill-formed.

    -4- Members of duration do not throw exceptions other than those thrown by the indicated operations on their representations.


4485(i). Move specification for task::stop_token_type

Section: 33.13.6.4 [task.state] Status: Tentatively Ready Submitter: Tomasz Kamiński Opened: 2025-12-08 Last modified: 2026-01-16

Priority: Not Prioritized

View all issues with Tentatively Ready status.

Discussion:

Addresses US 249-379

It is not clear what bullet 33.13.6.4 [task.state] p4.6 is – it reads like a requirement on stop_token_type, but if so, this paragraph is a poor place for it.

[2025-12-05 Tomasz comments]

The paragraph expresses requirements on user supplied Environment::stop_source_type, that needs to model stoppable-source, which includes stoppable-callback-for on associated token. We should also require that Environment::scheduler_type shall satisfy scheduler.

We also need to clarify that stop_possible and stop_requested on prom.token returns same value as st during lifetime of asynchronous operation.

[2026-01-16; Reflector poll.]

Set status to Tentatively Ready after six votes in favour during reflector poll.

Proposed resolution:

This wording is relative to N5014.

  1. Modify 33.13.6 [exec.task] as indicated:

    -5- allocator_type shall meet the Cpp17Allocator requirements, scheduler_type shall model scheduler, and stop_source_type shall model stoppable-source.

  2. Modify 33.13.6.4 [task.state] as indicated:

    void start() & noexcept;
    

    -4- Effects: Effects: Let prom be the object handle.promise(). Associates STATE(prom), RCVR(prom), and SCHED(prom) with *this as follows:

    • -4.1- […]
    • -4.2- […]
    • -4.3- […]
    Let st be get_stop_token(get_env(rcvr)). Initializes prom.token and prom.source such that during the lifetime of the asynchronous operation (33.3 [exec.async.ops]) associated with *this
    • -4.4- prom.token.stop_requested() returns st.stop_requested();
    • -4.5- prom.token.stop_possible() returns st.stop_possible();.
    • -4.6- for types Fn and Init such that both invocable<Fn> and constructible_from<Fn, Init> are modeled, stop_token_type::callback_type<Fn> models stoppable-callback-for<Fn, stop_token_type, Init>.


4491(i). Rename submdspan_extents and submdspan_canonicalize_slices

Section: 23.7.3.7 [mdspan.sub] Status: Tentatively Ready Submitter: Tomasz Kamiński Opened: 2025-12-16 Last modified: 2026-01-16

Priority: Not Prioritized

View all issues with Tentatively Ready status.

Discussion:

Addresses US 152-243 and PL-008.

Rename submdspan_extents to subextents and submdspan_canonicalize_slices to canonical_slices.

[2026-01-16; Reflector poll.]

Set status to Tentatively Ready after six votes in favour during reflector poll.

Proposed resolution:

This wording is relative to N5032.

  1. Modify 23.7.3.2 [mdspan.syn] as follows:

    // 23.7.3.7 [mdspan.sub], submdspan creation
    template<class OffsetType, class LengthType, class StrideType>
      struct strided_slice;
    
    template<class LayoutMapping>
      struct submdspan_mapping_result;
    
    struct full_extent_t { explicit full_extent_t() = default; };
    inline constexpr full_extent_t full_extent{};
    
    template<class IndexType, size_t... Extents, class... SliceSpecifiers>
      constexpr auto submdspan_extents(const extents<IndexType, Extents...>&, SliceSpecifiers...);
    
    //  [mdspan.sub.canonical], submdspan slice canonicalization
    template<class IndexType, size_t... Extents, class... Slices>
      constexpr auto submdspan_canonicalizecanonical_slices(const extents<IndexType, Extents...>& src,
                                                   Slices... slices);
    
  2. Modify [mdspan.sub.canonical] as follows:

    submdspan slice canonicalization

    template<class IndexType, size_t... Extents, class... Slices>
      constexpr auto submdspan_canonicalizecanonical_slices(const extents<IndexType, Extents...>& src,
                                                   Slices... slices);
    
  3. Modify 23.7.3.7.5 [mdspan.sub.extents] as follows:

    23.7.3.7.5 [mdspan.sub.extents] submdspan_extents function

    template<class IndexType, size_t... Extents, class... SliceSpecifiers>
      constexpr auto submdspan_extents(const extents<IndexType, Extents...>& src,
                                       SliceSpecifiers... raw_slices);
    

    -1- Let slices be the pack introduced by the following declaration:

      auto [...slices] = submdspan_canonicalizecanonical_slices(src, raw_slices...)
    

  4. Modify [mdspan.sub.map.sliceable] as follows:

    -5- Returns: An object smr of type SMR such that

    • -5.1- smr.mapping.extents() == submdspan_extents(m.extents(), valid_slices...) is true; and
    • -5.2- […]

  5. Modify 23.7.3.7.6.1 [mdspan.sub.map.common] as follows:

    -5- Let sub_ext be the result of submdspan_extents(extents(), slices...) and let SubExtents be decltype(sub_ext).

  6. Modify 23.7.3.7.7 [mdspan.sub.sub] as follows:

    -2- Let slices be the pack introduced by the following declaration:

      auto [...slices] = submdspan_canonicalizecanonical_slices(src, raw_slices...)
    


4494(i). Add consteval to std::meta::exception defaulted member functions

Section: 21.4.4 [meta.reflection.exception] Status: Tentatively NAD Submitter: Marek Polacek Opened: 2025-12-16 Last modified: 2026-01-28

Priority: Not Prioritized

View other active issues in [meta.reflection.exception].

View all other issues in [meta.reflection.exception].

View all issues with Tentatively NAD status.

Discussion:

CWG 3115 states that every function of consteval-only type shall be an immediate function. This caused a problem for std::meta::exception::what() which was marked constexpr but not consteval. To be able to mark it consteval, we had to tweak the rules about overriding by a consteval virtual function (CWG 3117).

But 21.4.4 [meta.reflection.exception] still defines std::meta::exception such that it contains these defaulted special member functions:

exception(const exception&) = default;
exception(exception&&) = default;

exception& operator=(const exception&) = default;
exception& operator=(exception&&) = default;

which aren't consteval (and since they're not templates, they won't be promoted to consteval as per P2564). I propose to make the four functions consteval:

consteval exception(const exception&) = default;
consteval exception(exception&&) = default;

consteval exception& operator=(const exception&) = default;
consteval exception& operator=(exception&&) = default;

[2026-01-28; Reflector poll. Status → Tentatively NAD.]

The second change in CWG 3115 fixes this.

Proposed resolution:

This wording is relative to N5032.

  1. Modify 21.4.4 [meta.reflection.exception] as indicated:

    namespace std::meta {
      class exception : public std::exception {
      private:
        optional<string> what_;  // exposition only
        u8string u8what_;        // exposition only
        info from_;              // exposition only
        source_location where_;  // exposition only
      public:
        consteval exception(u8string_view what, info from,
                            source_location where = source_location::current()) noexcept;
                            
        consteval exception(string_view what, info from,
                            source_location where = source_location::current()) noexcept;
                            
        consteval exception(const exception&) = default;
        consteval exception(exception&&) = default;
        
        consteval exception& operator=(const exception&) = default;
        consteval exception& operator=(exception&&) = default;
        
        constexpr const char* what() const noexcept override;
        consteval u8string_view u8what() const noexcept;
        consteval info from() const noexcept;
        consteval source_location where() const noexcept;
      };
    }
    

4500(i). constant_wrapper wording problems

Section: 21.3.5 [const.wrap.class] Status: Tentatively Ready Submitter: Matthias Wippich Opened: 2026-01-07 Last modified: 2026-01-16

Priority: Not Prioritized

View other active issues in [const.wrap.class].

View all other issues in [const.wrap.class].

View all issues with Tentatively Ready status.

Discussion:

During resolution of LWG 4383(i) prefix and postfix increment and decrement operators were changed to

template<constexpr-param T>
  constexpr auto operator++(this T) noexcept -> constant_wrapper<++Y>
    { return {}; }
template<constexpr-param T>
  constexpr auto operator++(this T, int) noexcept ->
    constant_wrapper<Y++> { return {}; }
template<constexpr-param T>
  constexpr auto operator--(this T) noexcept -> constant_wrapper<--Y>
    { return {}; }
template<constexpr-param T>
  constexpr auto operator--(this T, int) noexcept ->
    constant_wrapper<Y--> { return {}; }

However, we do not actually specify what Y is. Additionally, the assignment operator has been changed to

template<constexpr-param R>
  constexpr auto operator=(R) const noexcept
    -> constant_wrapper<X = R::value> { return {}; }

This is grammatically not valid C++. The assignment must be parenthesized.

[2026-01-16; Reflector poll.]

Set status to Tentatively Ready after five votes in favour during reflector poll.

Proposed resolution:

This wording is relative to N5032.

  1. Modify 21.3.5 [const.wrap.class], class template constant_wrapper synopsis, as indicated:

    struct cw-operators { // exposition only// pseudo-mutators
      template<constexpr-param T>
        constexpr auto operator++(this T) noexcept
          -> constant_wrapper<++Y (++T::value)> { return {}; }
      template<constexpr-param T>
        constexpr auto operator++(this T, int) noexcept
          -> constant_wrapper<Y++ (T::value++)> { return {}; }
      template<constexpr-param T>
        constexpr auto operator--(this T) noexcept
          -> constant_wrapper<--Y (--T::value)> { return {}; }
      template<constexpr-param T>
        constexpr auto operator--(this T, int) noexcept
          -> constant_wrapper<Y-- (T::value--)> { return {}; }
      …
    };
    
    template<cw-fixed-value X, class>
    struct constant_wrapper : cw-operators {
      static constexpr const auto & value = X.data;
      using type = constant_wrapper;
      using value_type = decltype(X)::type;
    
      template<constexpr-param R>
        constexpr auto operator=(R) const noexcept
          -> constant_wrapper<(X = R::value)> { return {}; }
      constexpr operator decltype(auto)() const noexcept { return value; }
    };