[PATCH v2 2/2] libstdc++: Runtime fallback for constant_wrapper subscript and call operators.

Jonathan Wakely jwakely@redhat.com
Tue Apr 14 11:32:07 GMT 2026


On Tue, 14 Apr 2026 at 12:13, Tomasz Kaminski <tkaminsk@redhat.com> wrote:
>
>
>
> On Tue, Apr 14, 2026 at 1:06 PM Jonathan Wakely <jwakely@redhat.com> wrote:
>>
>> On Tue, 07 Apr 2026 at 20:55 +0200, Tomasz Kamiński wrote:
>> >This implements P3978R3: constant_wrapper should unwrap on call and subscript.
>> >
>> >The operator() and operator[] are now fallbacking to calling corresponding
>> >operation on value, if either arguments are not constant_wrapper like, or
>> >the result of the invocation is not usable as non-type template argument
>> >(non-constant). The call operator is also now defined in terms of invoke,
>> >to support member pointers.
>> >
>> >The noexcept specification is simplified, by observing that creating a
>> >default cosntructed (constant_wrapper<value(....)>{}) is never throwing
>>
>> But the __invoke(value, args...) expression used for the template
>> argument could throw, couldn't it?
>>
>> Or the value[args...] expression.
>
> Yes, but evaluating a non-type template parameter starts a separate evaluator,
> and if it exits via exception, the type constant_wrapper<value(....)> is ill-formed,
> and we never go this path. It never get thrown from operator() evaluation.

Ah, thanks. The requires-expression already checked that the type is
valid, so if the  _ConstExprInvocable bool is true, we know that
determining the type didn't throw.

Good work on the detailed tests.

OK for trunk then, thanks.

>
> See: https://godbolt.org/z/5cMPTbd7W
>
>>
>>
>> >operation. Nested requires expr is used for short-circuting, and thus
>> >avoid checking viability of the operation on the value with
>> >constant_wrapper only (see PoisonedAdd, PoisonedIndex in tests).
>> >
>> >libstdc++-v3/ChangeLog:
>> >
>> >       * include/bits/utility.h (_CwOperators::operator())
>> >       (_CwOperators:operator[]): Delete, they are now provided by...
>> >       (constant_wrapper::operator(), constant_wrapper::operator[]):
>> >       Define.
>> >       * testsuite/20_util/constant_wrapper/generic.cc: Add additional
>> >       test cases for invoke and subscript.
>> >---
>> >v2:
>> > * forwards arguments in runtime path
>> > * added test for MoveOnly argument types
>> > * enable test_function_object and other test that will accidentally
>> >   disabled
>> > * add if !consteval for non-constexpr (R prefixed types)
>> >
>> >Tested *constant_wrapper* again. Testing full on x86_64-linux.
>> >OK for trunk when all test passes.
>> >
>> > libstdc++-v3/include/bits/utility.h           |  52 +++-
>> > .../20_util/constant_wrapper/generic.cc       | 293 ++++++++++++++----
>> > 2 files changed, 279 insertions(+), 66 deletions(-)
>> >
>> >diff --git a/libstdc++-v3/include/bits/utility.h b/libstdc++-v3/include/bits/utility.h
>> >index 970e63e8170..75576c0655c 100644
>> >--- a/libstdc++-v3/include/bits/utility.h
>> >+++ b/libstdc++-v3/include/bits/utility.h
>> >@@ -41,6 +41,9 @@
>> >
>> > #include <type_traits>
>> > #include <bits/move.h>
>> >+#ifdef __glibcxx_constant_wrapper // C++ >= 26
>> >+#  include <bits/invoke.h>
>> >+#endif
>> >
>> > namespace std _GLIBCXX_VISIBILITY(default)
>> > {
>> >@@ -342,19 +345,6 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>> >       -> constant_wrapper<_Left::value->*(_Right::value)>
>> >       { return {}; }
>> >
>> >-    template<_ConstExprParam _Tp, _ConstExprParam... _Args>
>> >-      constexpr auto
>> >-      operator()(this _Tp, _Args...) noexcept
>> >-      requires
>> >-      requires(_Args...) { constant_wrapper<_Tp::value(_Args::value...)>(); }
>> >-      { return constant_wrapper<_Tp::value(_Args::value...)>{}; }
>> >-
>> >-    template<_ConstExprParam _Tp, _ConstExprParam... _Args>
>> >-      constexpr auto
>> >-      operator[](this _Tp, _Args...) noexcept
>> >-      -> constant_wrapper<(_Tp::value[_Args::value...])>
>> >-      { return {}; }
>> >-
>> >     template<_ConstExprParam _Tp>
>> >       constexpr auto
>> >       operator++(this _Tp) noexcept
>> >@@ -453,6 +443,42 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>> >       -> constant_wrapper<(value = _Right::value)>
>> >       { return {}; }
>> >
>> >+    template<typename... _Args,
>> >+           bool _ConstExprInvocable = requires {
>> >+             requires (_ConstExprParam<remove_cvref_t<_Args>> && ...);
>> >+             typename constant_wrapper<std::__invoke(value, remove_cvref_t<_Args>::value...)>;
>> >+           }>
>> >+      requires _ConstExprInvocable || is_invocable_v<const value_type&, _Args...>
>> >+      static constexpr decltype(auto)
>> >+      operator()(_Args&&... __args)
>> >+      noexcept(requires {
>> >+      requires _ConstExprInvocable || is_nothrow_invocable_v<const value_type&, _Args...>;
>> >+      })
>> >+      {
>> >+      if constexpr (_ConstExprInvocable)
>> >+        return constant_wrapper<std::__invoke(value, remove_cvref_t<_Args>::value...)>{};
>> >+      else
>> >+        return std::__invoke(value, std::forward<_Args>(__args)...);
>> >+      }
>> >+
>> >+    template<typename... _Args,
>> >+           bool _ConstExprSubscriptable = requires {
>> >+             requires (_ConstExprParam<remove_cvref_t<_Args>> && ...);
>> >+             typename constant_wrapper<value[remove_cvref_t<_Args>::value...]>;
>> >+           }>
>> >+      requires _ConstExprSubscriptable || requires { value[std::declval<_Args>()...]; }
>> >+      static constexpr decltype(auto)
>> >+      operator[](_Args&&... __args)
>> >+      noexcept(requires {
>> >+      requires _ConstExprSubscriptable || noexcept(value[std::declval<_Args>()...]);
>> >+      })
>> >+      {
>> >+      if constexpr (_ConstExprSubscriptable)
>> >+        return constant_wrapper<value[remove_cvref_t<_Args>::value...]>{};
>> >+      else
>> >+        return value[std::forward<_Args>(__args)...];
>> >+      }
>> >+
>> >     constexpr
>> >     operator decltype(value)() const noexcept
>> >     { return value; }
>> >diff --git a/libstdc++-v3/testsuite/20_util/constant_wrapper/generic.cc b/libstdc++-v3/testsuite/20_util/constant_wrapper/generic.cc
>> >index 1c7770d7a89..1773185ab2b 100644
>> >--- a/libstdc++-v3/testsuite/20_util/constant_wrapper/generic.cc
>> >+++ b/libstdc++-v3/testsuite/20_util/constant_wrapper/generic.cc
>> >@@ -1,4 +1,5 @@
>> > // { dg-do run { target c++26 } }
>> >+#include <functional>
>> > #include <utility>
>> > #include <string_view>
>> >
>> >@@ -20,9 +21,9 @@ test_c_arrays()
>> >   auto access = [](auto x, size_t i)
>> >   { return x[i]; };
>> >
>> >-  check_same(access(std::cw<x>, 0), x[0]);
>> >-  check_same(access(std::cw<x>, 1), x[1]);
>> >-  check_same(access(std::cw<x>, 2), x[2]);
>> >+  check_same(std::cw<x>[0], x[0]);
>> >+  check_same(std::cw<x>[1], x[1]);
>> >+  check_same(std::cw<x>[2], x[2]);
>> >
>> >   check_same(cx[std::cw<0>], std::cw<x[0]>);
>> >   check_same(cx[std::cw<1>], std::cw<x[1]>);
>> >@@ -96,76 +97,254 @@ constexpr int
>> > add(int i, int j)
>> > { return i + j; }
>> >
>> >-struct Add
>> >+template<bool Noexcept>
>> >+struct CAdd
>> > {
>> >   constexpr int
>> >-  operator()(int i, int j) const noexcept
>> >+  operator()(int i, int j) const noexcept(Noexcept)
>> >   { return i + j; }
>> > };
>> >
>> >-constexpr void
>> >-test_function_object()
>> >+template<bool Noexcept>
>> >+struct RAdd
>> > {
>> >-  auto check = [](auto cfo)
>> >+  int
>> >+  operator()(int i, int j) const noexcept(Noexcept)
>> >+  { return i + j; }
>> >+};
>> >+
>> >+struct CMixedAdd
>> >+{
>> >+  static constexpr int
>> >+  operator()(int i, int j)
>> >+  { return i + j; }
>> >+
>> >+  template <auto N1, auto N2>
>> >+    static constexpr int operator()
>> >+    (std::constant_wrapper<N1, int> i, std::constant_wrapper<N2, int> j)
>> >+    { return 100 + i + j; }
>> >+};
>> >+
>> >+struct RMixedAdd
>> >+{
>> >+  static int
>> >+  operator()(int i, int j)
>> >+  { return i + j; }
>> >+
>> >+  template <auto N1, auto N2>
>> >+    static int operator()
>> >+    (std::constant_wrapper<N1, int> i, std::constant_wrapper<N2, int> j)
>> >+    { return 100 + i + j; }
>> >+};
>> >+
>> >+template<typename T1, typename T2>
>> >+struct AtLeastOneInt
>> >+{
>> >+  static_assert(std::is_same_v<T1, int> || std::is_same_v<T2, int>);
>> >+  using type = int;
>> >+};
>> >+
>> >+struct PoisonedAdd
>> >+{
>> >+  template<typename T1, typename T2>
>> >+  constexpr static
>> >+  typename AtLeastOneInt<T1, T2>::type
>> >+  operator()(T1 i, T2 j) noexcept
>> >+  { return i + j; }
>> >+};
>> >+
>> >+struct MoveOnly
>> >+{
>> >+  constexpr explicit
>> >+  MoveOnly(int p) : v(p)
>> >+  { }
>> >+
>> >+  MoveOnly(MoveOnly&&) = default;
>> >+
>> >+  int v;
>> >+};
>> >+
>> >+struct MoveArgFunc
>> >+{
>> >+  constexpr int
>> >+  operator()(MoveOnly arg) const
>> >+  { return arg.v; }
>> >+};
>> >+
>> >+template<bool Constexpr, auto functor>
>> >+  constexpr void
>> >+  check_invoke()
>> >   {
>> >     auto ci = std::cw<2>;
>> >     auto cj = std::cw<3>;
>> >+    auto cfo = std::cw<functor>;
>> >
>> >-    VERIFY(cfo(ci, cj) == 5);
>> >-    static_assert(std::same_as<decltype(cfo(ci, cj)), std::constant_wrapper<5>>);
>> >+    if constexpr (Constexpr)
>> >+      check_same(cfo(ci, cj), std::cw<5>);
>> >+    else
>> >+      check_same(cfo(ci, cj), 5);
>> >
>> >-    static_assert(std::invocable<decltype(cfo), decltype(ci), decltype(cj)>);
>> >-    static_assert(!std::invocable<decltype(cfo), int, decltype(cj)>);
>> >-    static_assert(!std::invocable<decltype(cfo), int, int>);
>> >-  };
>> >+    check_same(cfo(2, cj), 5);
>> >+    check_same(cfo(2, 3), 5);
>> >+
>> >+    constexpr bool Noexcept = noexcept(functor(2, 3));
>> >+    static_assert(noexcept(cfo(ci, cj)) == Constexpr || Noexcept);
>> >+    static_assert(noexcept(cfo(2, cj)) == Noexcept);
>> >+    static_assert(noexcept(cfo(2, 3)) == Noexcept);
>> >+  }
>> >
>> >-  check(std::cw<Add{}>);
>> >-  check(std::cw<[](int i, int j){ return i + j; }>);
>> >-  check(std::cw<[](auto i, auto j){ return i + j; }>);
>> >+constexpr void
>> >+test_function_object()
>> >+{
>> >+  check_invoke<true, CAdd<true>{}>();
>> >+  check_invoke<true, CAdd<false>{}>();
>> >+  check_invoke<true, [](int i, int j) { return i + j; }>();
>> >+  check_invoke<true, [](auto i, auto j) noexcept { return i + j; }>();
>> >+  if !consteval {
>> >+    check_invoke<false, RAdd<true>{}>();
>> >+    check_invoke<false, RAdd<false>{}>();
>> >+  }
>> >+
>> >+  // Check if constant_wrappers are not passed to value,
>> >+  // if they can be unwrapped.
>> >+  check_invoke<true, PoisonedAdd{}>();
>> >+
>> >+  // Prefer unwrapping constant_wrappers, so calls (int, int)
>> >+  check_same(CMixedAdd{}(2, 3), 5);
>> >+  check_same(CMixedAdd{}(std::cw<2>, std::cw<3>), 105);
>> >+  check_same(std::cw<CMixedAdd{}>(std::cw<2>, std::cw<3>), std::cw<5>);
>> >+  check_invoke<true, CMixedAdd{}>();
>> >+  if !consteval {
>> >+    // Cannot return value wrapped in constant_wrapper because operator
>> >+    // is not constexpr, fallbacks to runtime call, that selects
>> >+    // (constant_wrapper, constant_wrapper) overload
>> >+    check_same(RMixedAdd{}(2, 3), 5);
>> >+    check_same(RMixedAdd{}(std::cw<2>, std::cw<3>), 105);
>> >+    check_same(std::cw<RMixedAdd{}>(std::cw<2>, std::cw<3>), 105);
>> >+    check_same(std::cw<RMixedAdd{}>(std::cw<2>, 3), 5);
>> >+    check_same(std::cw<RMixedAdd{}>(2, 3), 5);
>> >+  }
>> >+
>> >+  // Test if arguments are fowarded
>> >+  std::cw<MoveArgFunc{}>(MoveOnly{10});
>> > }
>> >
>> > constexpr void
>> > test_function_pointer()
>> > {
>> >-  auto cptr = std::cw<add>;
>> >-  auto ci = std::cw<2>;
>> >-  auto cj = std::cw<3>;
>> >+  check_invoke<true, add>();
>> >+}
>> >
>> >-  VERIFY(cptr(ci, cj) == 5);
>> >-  static_assert(std::same_as<decltype(cptr(ci, cj)), std::constant_wrapper<5>>);
>> >+template<bool Noexcept>
>> >+struct CIndex
>> >+{
>> >+  constexpr int
>> >+  operator[](int i, int j) const noexcept(Noexcept)
>> >+  { return i*j; }
>> >+};
>> >
>> >-  VERIFY(cptr(2, cj) == 5);
>> >-  static_assert(std::same_as<decltype(cptr(2, cj)), int>);
>> >+template<bool Noexcept>
>> >+struct RIndex
>> >+{
>> >+  int
>> >+  operator[](int i, int j) const noexcept(Noexcept)
>> >+  { return i*j; }
>> >+};
>> >
>> >-  VERIFY(cptr(2, 3) == 5);
>> >-  static_assert(std::same_as<decltype(cptr(2, 3)), int>);
>> >-}
>> >+struct CMixedIndex
>> >+{
>> >+  static constexpr int
>> >+  operator[](int i, int j)
>> >+  { return i * j; }
>> >+
>> >+  template <auto N1, auto N2>
>> >+    static constexpr int operator[]
>> >+    (std::constant_wrapper<N1, int> i, std::constant_wrapper<N2, int> j)
>> >+    { return 100 + i * j; }
>> >+};
>> >
>> >-struct Indexable1
>> >+struct RMixedIndex
>> >+{
>> >+  static int
>> >+  operator[](int i, int j)
>> >+  { return i * j; }
>> >+
>> >+  template <auto N1, auto N2>
>> >+    static int operator[]
>> >+    (std::constant_wrapper<N1, int> i, std::constant_wrapper<N2, int> j)
>> >+    { return 100 + i * j; }
>> >+};
>> >+
>> >+struct PoisonedIndex
>> >+{
>> >+  template<typename T1, typename T2>
>> >+  constexpr static
>> >+  typename AtLeastOneInt<T1, T2>::type
>> >+  operator[](T1 i, T2 j) noexcept
>> >+  { return i * j; }
>> >+};
>> >+
>> >+struct MoveArgIndex
>> > {
>> >   constexpr int
>> >-  operator[](int i, int j) const noexcept
>> >-  { return i*j; }
>> >+  operator[](MoveOnly arg) const
>> >+  { return arg.v; }
>> > };
>> >
>> >-template<typename Obj, typename... Args>
>> >-  concept indexable = requires (Obj obj, Args... args)
>> >+template<bool Constexpr, auto index>
>> >+  constexpr void
>> >+  check_subscript()
>> >   {
>> >-    obj[args...];
>> >-  };
>> >+    auto ci = std::cw<2>;
>> >+    auto cj = std::cw<3>;
>> >+    auto cio = std::cw<index>;
>> >+
>> >+    if constexpr (Constexpr)
>> >+      check_same(cio[ci, cj], std::cw<6>);
>> >+    else
>> >+      check_same(cio[ci, cj], 6);
>> >+
>> >+    check_same(cio[2, cj], 6);
>> >+    check_same(cio[2, 3], 6);
>> >+
>> >+    constexpr bool Noexcept = noexcept(index[2, 3]);
>> >+    static_assert(noexcept(cio[ci, cj]) == Constexpr || Noexcept);
>> >+    static_assert(noexcept(cio[2, cj]) == Noexcept);
>> >+    static_assert(noexcept(cio[2, 3]) == Noexcept);
>> >+  }
>> >
>> > constexpr void
>> > test_indexable1()
>> > {
>> >-  auto cind = std::cw<Indexable1{}>;
>> >-  auto ci = std::cw<2>;
>> >-  auto cj = std::cw<3>;
>> >-  VERIFY(cind[ci, cj] == ci*cj);
>> >-  static_assert(std::same_as<decltype(cind[ci, cj]), std::constant_wrapper<6>>);
>> >-
>> >-  static_assert(indexable<decltype(cind), decltype(ci), decltype(cj)>);
>> >-  static_assert(!indexable<decltype(cind), int, decltype(cj)>);
>> >-  static_assert(!indexable<decltype(cind), int, int>);
>> >+  check_subscript<true, CIndex<true>{}>();
>> >+  check_subscript<true, CIndex<false>{}>();
>> >+  if !consteval {
>> >+    check_subscript<false, RIndex<true>{}>();
>> >+    check_subscript<false, RIndex<false>{}>();
>> >+  }
>> >+
>> >+  // Check if constant_wrappers are not passed to value,
>> >+  // if they can be unwrapped.
>> >+  check_subscript<true, PoisonedIndex{}>();
>> >+
>> >+  // Prefer unwrapping constant_wrappers, so calls (int, int)
>> >+  check_same(CMixedIndex{}[2, 3], 6);
>> >+  check_same(CMixedIndex{}[std::cw<2>, std::cw<3>], 106);
>> >+  check_same(std::cw<CMixedIndex{}>[std::cw<2>, std::cw<3>], std::cw<6>);
>> >+  check_subscript<true, CMixedIndex{}>();
>> >+  if !consteval {
>> >+    // Cannot return value wrapped in constant_wrapper because operator
>> >+    // is not constexpr, fallbacks to runtime call, that selects
>> >+    // (constant_wrapper, constant_wrapper) overload
>> >+    check_same(RMixedIndex{}[2, 3], 6);
>> >+    check_same(RMixedIndex{}[std::cw<2>, std::cw<3>], 106);
>> >+    check_same(std::cw<RMixedIndex{}>[std::cw<2>, std::cw<3>], 106);
>> >+    check_same(std::cw<RMixedIndex{}>[std::cw<2>, 3], 6);
>> >+    check_same(std::cw<RMixedIndex{}>[2, 3], 6);
>> >+  }
>> >+
>> >+  // Test if arguments are fowarded
>> >+  std::cw<MoveArgIndex{}>[MoveOnly{10}];
>> > }
>> >
>> > struct Indexable2
>> >@@ -182,12 +361,9 @@ test_indexable2()
>> >   auto cind = std::cw<Indexable2{}>;
>> >   auto ci = std::cw<2>;
>> >   auto cj = std::cw<3>;
>> >-  VERIFY(cind[ci, cj] == ci*cj);
>> >-  static_assert(std::same_as<decltype(cind[ci, cj]), std::constant_wrapper<6>>);
>> >-
>> >-  static_assert(indexable<decltype(cind), decltype(ci), decltype(cj)>);
>> >-  static_assert(!indexable<decltype(cind), int, decltype(cj)>);
>> >-  static_assert(!indexable<decltype(cind), int, int>);
>> >+  check_same(cind[ci, cj], std::cw<6>);
>> >+  check_same(cind[ci, 3], 6);
>> >+  check_same(cind[2, 3], 6);
>> > }
>> >
>> > struct Indexable3
>> >@@ -234,10 +410,21 @@ test_member_pointer()
>> >   check_same((&co)->*(&Divide::value), nom);
>> >   check_same(&(co.value)->*cvalue, nom);
>> >
>> >-  auto expect_unwrapped = nom / denom;
>> >-  check_same(((&co)->*(&Divide::divide))(denom), expect_unwrapped);
>> >-  check_same((&(co.value)->*cdiv)(denom), expect_unwrapped);
>> >-  check_same(((&decltype(co)::value)->*cdiv)(denom), expect_unwrapped);
>> >+  check_same(cvalue(co), std::cw<nom>);
>> >+  check_same(cvalue(co.value), nom);
>> >+  check_same(cvalue(&co.value), nom);
>> >+  check_same(cvalue(std::ref(co.value)), nom);
>> >+
>> >+  auto cresult = std::cw<nom / denom>;
>> >+  check_same(((&co)->*(&Divide::divide))(denom), cresult.value);
>> >+  check_same((&(co.value)->*cdiv)(denom), cresult.value);
>> >+  check_same(((&decltype(co)::value)->*cdiv)(denom), cresult.value);
>> >+
>> >+  check_same(cdiv(co, std::cw<denom>), cresult);
>> >+  check_same(cdiv(co.value, std::cw<denom>), cresult.value);
>> >+  check_same(cdiv(co.value, denom), cresult.value);
>> >+  check_same(cdiv(&co.value, denom), cresult.value);
>> >+  check_same(cdiv(std::ref(co.value), denom), cresult.value);
>> > }
>> >
>> > struct Truthy
>> >--
>> >2.53.0
>> >
>> >
>>



More information about the Libstdc++ mailing list