[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