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

Jonathan Wakely jwakely@redhat.com
Tue Apr 14 11:06:38 GMT 2026


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.

>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