[PATCH v2] libstdc++: Pass small trivial types by value in polymorphic wrappers
Jonathan Wakely
jwakely@redhat.com
Mon Jun 2 11:29:34 GMT 2025
On Thu, 29 May 2025 at 07:49, Tomasz Kamiński <tkaminsk@redhat.com> wrote:
>
> This patch adjust the passing of parameters for the move_only_function,
> copyable_function and function_ref. For types that are declared as being passed
> by value in signature template argument, they are passed by value to the invoker,
> when they are small (at most two pointers), trivially move constructible and
> trivially destructible. The latter guarantees that passing them by value has not
> user visible side effects.
>
> In particular, this extends the set of types forwarded by value, that was
> previously limited to scalars, to also include specializations of std::span and
> std::string_view, and similar standard and program defined-types.
>
> Checking the suitability of the parameter types requires the types to be complete.
> As a consequence, the implementation imposes requirements on instantiation of
> move_only_function and copyable_function. To avoid producing the errors from
> the implementation details, a static assertion was added to partial
> specializations of copyable_function, move_only_function and function_ref.
> The static assertion uses existing __is_complete_or_unbounded, as arrays type
> parameters are automatically decayed in function type.
>
> Standard already specifies in [res.on.functions] p2.5 that instantiating these
> partial specialization with incomplete types leads to undefined behavior.
>
> libstdc++-v3/ChangeLog:
>
> * include/bits/funcwrap.h (__polyfunc::__pass_by_rref): Define.
> (__polyfunc::__param_t): Update to use __pass_by_rref.
> * include/bits/cpyfunc_impl.h:: Assert that are parameters type
> are complete.
> * include/bits/funcref_impl.h: Likewise.
> * include/bits/mofunc_impl.h: Likewise.
> * testsuite/20_util/copyable_function/call.cc: New test.
> * testsuite/20_util/function_ref/call.cc: New test.
> * testsuite/20_util/move_only_function/call.cc: New test.
> * testsuite/20_util/copyable_function/conv.cc: New test.
> * testsuite/20_util/function_ref/conv.cc: New test.
> * testsuite/20_util/move_only_function/conv.cc: New test.
> * testsuite/20_util/copyable_function/incomplete_neg.cc: New test.
> * testsuite/20_util/function_ref/incomplete_neg.cc: New test.
> * testsuite/20_util/move_only_function/incomplete_neg.cc: New test.
>
> Reviewed-by: Patrick Palka <ppalka@redhat.com>
> ---
> Changes in v3:
> - reworked pass_by_rref to pass_by_value, and simplified impl
> - fixed some typos in the commit message
>
> OK for trunk?
OK, thanks.
>
> libstdc++-v3/include/bits/cpyfunc_impl.h | 4 +++
> libstdc++-v3/include/bits/funcref_impl.h | 4 +++
> libstdc++-v3/include/bits/funcwrap.h | 20 ++++++++++-
> libstdc++-v3/include/bits/mofunc_impl.h | 4 +++
> .../20_util/copyable_function/call.cc | 7 ++--
> .../20_util/copyable_function/conv.cc | 35 +++++++++++++++++++
> .../copyable_function/incomplete_neg.cc | 18 ++++++++++
> .../testsuite/20_util/function_ref/call.cc | 10 +++---
> .../testsuite/20_util/function_ref/conv.cc | 34 ++++++++++++++++++
> .../20_util/function_ref/incomplete_neg.cc | 18 ++++++++++
> .../20_util/move_only_function/call.cc | 7 ++--
> .../20_util/move_only_function/conv.cc | 35 +++++++++++++++++++
> .../move_only_function/incomplete_neg.cc | 18 ++++++++++
> 13 files changed, 202 insertions(+), 12 deletions(-)
> create mode 100644 libstdc++-v3/testsuite/20_util/copyable_function/incomplete_neg.cc
> create mode 100644 libstdc++-v3/testsuite/20_util/function_ref/incomplete_neg.cc
> create mode 100644 libstdc++-v3/testsuite/20_util/move_only_function/incomplete_neg.cc
>
> diff --git a/libstdc++-v3/include/bits/cpyfunc_impl.h b/libstdc++-v3/include/bits/cpyfunc_impl.h
> index bc44cd3e313..f1918ddf87a 100644
> --- a/libstdc++-v3/include/bits/cpyfunc_impl.h
> +++ b/libstdc++-v3/include/bits/cpyfunc_impl.h
> @@ -64,6 +64,10 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> _GLIBCXX_MOF_REF noexcept(_Noex)>
> : __polyfunc::_Cpy_base
> {
> + static_assert(
> + (std::__is_complete_or_unbounded(__type_identity<_ArgTypes>()) && ...),
> + "each parameter type must be a complete class");
> +
> using _Base = __polyfunc::_Cpy_base;
> using _Invoker = __polyfunc::_Invoker<_Noex, _Res, _ArgTypes...>;
> using _Signature = _Invoker::_Signature;
> diff --git a/libstdc++-v3/include/bits/funcref_impl.h b/libstdc++-v3/include/bits/funcref_impl.h
> index 1e19866035f..44c992281be 100644
> --- a/libstdc++-v3/include/bits/funcref_impl.h
> +++ b/libstdc++-v3/include/bits/funcref_impl.h
> @@ -68,6 +68,10 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> class function_ref<_Res(_ArgTypes...) _GLIBCXX_MOF_CV
> noexcept(_Noex)>
> {
> + static_assert(
> + (std::__is_complete_or_unbounded(__type_identity<_ArgTypes>()) && ...),
> + "each parameter type must be a complete class");
> +
> using _Invoker = __polyfunc::_Invoker<_Noex, _Res, _ArgTypes...>;
> using _Signature = _Invoker::_Signature;
>
> diff --git a/libstdc++-v3/include/bits/funcwrap.h b/libstdc++-v3/include/bits/funcwrap.h
> index cf261bcd4c8..9db4ab7811a 100644
> --- a/libstdc++-v3/include/bits/funcwrap.h
> +++ b/libstdc++-v3/include/bits/funcwrap.h
> @@ -199,7 +199,25 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> };
>
> template<typename _Tp>
> - using __param_t = __conditional_t<is_scalar_v<_Tp>, _Tp, _Tp&&>;
> + consteval bool
> + __pass_by_value()
> + {
> + // n.b. sizeof(Incomplete&) is ill-formed for incomplete types,
> + // so we check is_reference_v first.
> + if constexpr (is_reference_v<_Tp> || is_scalar_v<_Tp>)
> + return true;
> + else
> + // n.b. we already asserted that types are complete in wrappers,
> + // avoid triggering additional errors from this function.
> + if constexpr (std::__is_complete_or_unbounded(__type_identity<_Tp>()))
> + if constexpr (sizeof(_Tp) <= 2 * sizeof(void*))
> + return is_trivially_move_constructible_v<_Tp>
> + && is_trivially_destructible_v<_Tp>;
> + return false;
> + }
> +
> + template<typename _Tp>
> + using __param_t = __conditional_t<__pass_by_value<_Tp>(), _Tp, _Tp&&>;
>
> template<bool _Noex, typename _Ret, typename... _Args>
> using _Invoker = _Base_invoker<_Noex, remove_cv_t<_Ret>, __param_t<_Args>...>;
> diff --git a/libstdc++-v3/include/bits/mofunc_impl.h b/libstdc++-v3/include/bits/mofunc_impl.h
> index 1ceb910e815..468e6855fc6 100644
> --- a/libstdc++-v3/include/bits/mofunc_impl.h
> +++ b/libstdc++-v3/include/bits/mofunc_impl.h
> @@ -64,6 +64,10 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> _GLIBCXX_MOF_REF noexcept(_Noex)>
> : __polyfunc::_Mo_base
> {
> + static_assert(
> + (std::__is_complete_or_unbounded(__type_identity<_ArgTypes>()) && ...),
> + "each parameter type must be a complete class");
> +
> using _Base = __polyfunc::_Mo_base;
> using _Invoker = __polyfunc::_Invoker<_Noex, _Res, _ArgTypes...>;
> using _Signature = _Invoker::_Signature;
> diff --git a/libstdc++-v3/testsuite/20_util/copyable_function/call.cc b/libstdc++-v3/testsuite/20_util/copyable_function/call.cc
> index cf997577f62..0ac5348f2fe 100644
> --- a/libstdc++-v3/testsuite/20_util/copyable_function/call.cc
> +++ b/libstdc++-v3/testsuite/20_util/copyable_function/call.cc
> @@ -204,13 +204,14 @@ test05()
> }
>
> struct Incomplete;
> +enum CompleteEnum : int;
>
> void
> test_params()
> {
> - std::copyable_function<void(Incomplete)> f1;
> - std::copyable_function<void(Incomplete&)> f2;
> - std::copyable_function<void(Incomplete&&)> f3;
> + std::copyable_function<void(Incomplete&)> f1;
> + std::copyable_function<void(Incomplete&&)> f2;
> + std::copyable_function<void(CompleteEnum)> f4;
> }
>
> int main()
> diff --git a/libstdc++-v3/testsuite/20_util/copyable_function/conv.cc b/libstdc++-v3/testsuite/20_util/copyable_function/conv.cc
> index e678e166172..11c839b6932 100644
> --- a/libstdc++-v3/testsuite/20_util/copyable_function/conv.cc
> +++ b/libstdc++-v3/testsuite/20_util/copyable_function/conv.cc
> @@ -2,6 +2,7 @@
> // { dg-require-effective-target hosted }
>
> #include <functional>
> +#include <string_view>
> #include <testsuite_hooks.h>
>
> using std::copyable_function;
> @@ -15,6 +16,21 @@ static_assert( !std::is_constructible_v<std::copyable_function<void()&>,
> static_assert( !std::is_constructible_v<std::copyable_function<void() const>,
> std::copyable_function<void()>> );
>
> +using FuncType = int(int);
> +
> +// Top level const qualifiers are ignored and decay is performed in parameters
> +// of function_types.
> +static_assert( std::is_same_v<std::copyable_function<void(int const)>,
> + std::copyable_function<void(int)>> );
> +static_assert( std::is_same_v<std::copyable_function<void(int[2])>,
> + std::copyable_function<void(int*)>>);
> +static_assert( std::is_same_v<std::copyable_function<void(int[])>,
> + std::copyable_function<void(int*)>>);
> +static_assert( std::is_same_v<std::copyable_function<void(int const[5])>,
> + std::copyable_function<void(int const*)>>);
> +static_assert( std::is_same_v<std::copyable_function<void(FuncType)>,
> + std::copyable_function<void(FuncType*)>>);
> +
> // Non-trivial args, guarantess that type is not passed by copy
> struct CountedArg
> {
> @@ -240,6 +256,24 @@ test06()
> VERIFY( f2(c) == 2 );
> }
>
> +void
> +test07()
> +{
> + // Scalar types and small trivially move constructible types are passed
> + // by value to invoker. So int&& signature is not compatible for such types.
> + auto fi = [](CountedArg const& arg, int) noexcept { return arg.counter; };
> + std::copyable_function<int(CountedArg, int) const noexcept> ci1(fi);
> + VERIFY( ci1(c, 0) == 1 );
> + std::copyable_function<int(CountedArg, int&&) const noexcept> ci2(ci1);
> + VERIFY( ci2(c, 0) == 2 );
> +
> + auto fs = [](CountedArg const& arg, std::string_view) noexcept { return arg.counter; };
> + std::copyable_function<int(CountedArg, std::string_view) const noexcept> cs1(fs);
> + VERIFY( cs1(c, "") == 1 );
> + std::copyable_function<int(CountedArg, std::string_view&&) const noexcept> cs2(cs1);
> + VERIFY( cs2(c, "") == 2 );
> +}
> +
> int main()
> {
> test01();
> @@ -248,4 +282,5 @@ int main()
> test04();
> test05();
> test06();
> + test07();
> }
> diff --git a/libstdc++-v3/testsuite/20_util/copyable_function/incomplete_neg.cc b/libstdc++-v3/testsuite/20_util/copyable_function/incomplete_neg.cc
> new file mode 100644
> index 00000000000..21ddde0d2a1
> --- /dev/null
> +++ b/libstdc++-v3/testsuite/20_util/copyable_function/incomplete_neg.cc
> @@ -0,0 +1,18 @@
> +// { dg-do compile { target c++26 } }
> +
> +#include <functional>
> +
> +struct IncompleteClass;
> +
> +using T1 = std::copyable_function<int(IncompleteClass)>::result_type; // { dg-error "here" }
> +using T2 = std::copyable_function<int(int, IncompleteClass)>::result_type; // { dg-error "here" }
> +
> +enum Enum {
> + x = [] {
> + // Enum enumeration is incomplete here
> + using T3 = std::copyable_function<int(Enum)>::result_type; // { dg-error "here" }
> + return T3(1);
> + }()
> +};
> +
> +// { dg-error "static assertion failed" "" { target *-*-* } 0 }
> diff --git a/libstdc++-v3/testsuite/20_util/function_ref/call.cc b/libstdc++-v3/testsuite/20_util/function_ref/call.cc
> index a91c6b4abb9..23253c33ee3 100644
> --- a/libstdc++-v3/testsuite/20_util/function_ref/call.cc
> +++ b/libstdc++-v3/testsuite/20_util/function_ref/call.cc
> @@ -164,16 +164,16 @@ test05()
> }
>
> struct Incomplete;
> +enum CompleteEnum : int;
>
> void
> test_params()
> {
> auto f = [](auto&&) {};
> - // There is discussion if this is supported.
> - // std::function_ref<void(Incomplete)> f1(f);
> - std::function_ref<void(Incomplete&)> f2(f);
> - // See PR120259, this should be supported.
> - // std::function_ref<void(Incomplete&&)> f3(f);
> + std::function_ref<void(Incomplete&)> f1(f);
> + // See PR libstdc++/120259, this should be supported.
> + // std::function_ref<void(Incomplete&&)> f2(f);
> + std::function_ref<void(CompleteEnum)> f3(f);
> }
>
> int main()
> diff --git a/libstdc++-v3/testsuite/20_util/function_ref/conv.cc b/libstdc++-v3/testsuite/20_util/function_ref/conv.cc
> index 7dc7b8ceac0..7606d265f98 100644
> --- a/libstdc++-v3/testsuite/20_util/function_ref/conv.cc
> +++ b/libstdc++-v3/testsuite/20_util/function_ref/conv.cc
> @@ -2,6 +2,7 @@
> // { dg-require-effective-target hosted }
>
> #include <functional>
> +#include <string_view>
> #include <testsuite_hooks.h>
>
> using std::function_ref;
> @@ -20,6 +21,21 @@ struct CountedArg
> };
> CountedArg const c;
>
> +using FuncType = int(int);
> +
> +// Top level const qualifiers are ignored in function types, and decay
> +// is performed.
> +static_assert( std::is_same_v<std::function_ref<void(int const)>,
> + std::function_ref<void(int)>> );
> +static_assert( std::is_same_v<std::function_ref<void(int[2])>,
> + std::function_ref<void(int*)>>);
> +static_assert( std::is_same_v<std::function_ref<void(int[])>,
> + std::function_ref<void(int*)>>);
> +static_assert( std::is_same_v<std::function_ref<void(int const[5])>,
> + std::function_ref<void(int const*)>>);
> +static_assert( std::is_same_v<std::function_ref<void(FuncType)>,
> + std::function_ref<void(FuncType*)>>);
> +
> // The C++26 [func.wrap.general] p2 does not currently cover funciton_ref,
> // so we make extra copies of arguments.
>
> @@ -244,6 +260,23 @@ test08()
> return true;
> };
>
> +void
> +test09()
> +{
> + // Scalar types and small trivially move constructible types are passed
> + // by value to invoker. So int&& signature is not compatible for such types.
> + auto fi = [](CountedArg const& arg, int) noexcept { return arg.counter; };
> + std::function_ref<int(CountedArg, int) const noexcept> ri1(fi);
> + VERIFY( ri1(c, 0) == 1 );
> + std::function_ref<int(CountedArg, int&&) const noexcept> ri2(ri1);
> + VERIFY( ri2(c, 0) == 2 );
> +
> + auto fs = [](CountedArg const& arg, std::string_view) noexcept { return arg.counter; };
> + std::function_ref<int(CountedArg, std::string_view) const noexcept> rs1(fs);
> + VERIFY( rs1(c, "") == 1 );
> + std::function_ref<int(CountedArg, std::string_view&&) const noexcept> rs2(rs1);
> + VERIFY( rs2(c, "") == 2 );
> +}
>
> int main()
> {
> @@ -254,6 +287,7 @@ int main()
> test05();
> test06();
> test07();
> + test09();
>
> static_assert( test08() );
> }
> diff --git a/libstdc++-v3/testsuite/20_util/function_ref/incomplete_neg.cc b/libstdc++-v3/testsuite/20_util/function_ref/incomplete_neg.cc
> new file mode 100644
> index 00000000000..c8db1eee42c
> --- /dev/null
> +++ b/libstdc++-v3/testsuite/20_util/function_ref/incomplete_neg.cc
> @@ -0,0 +1,18 @@
> +// { dg-do compile { target c++26 } }
> +
> +#include <functional>
> +
> +struct IncompleteClass;
> +
> +int a1 = alignof(std::function_ref<int(IncompleteClass)>); // { dg-error "here" }
> +int a2 = alignof(std::function_ref<int(int, IncompleteClass)>); // { dg-error "here" }
> +
> +enum Enum {
> + x = [] {
> + // Enum enumeration is incomplete here
> + int a3 = alignof(std::function_ref<int(Enum)>); // { dg-error "here" }
> + return 1;
> + }()
> +};
> +
> +// { dg-error "static assertion failed" "" { target *-*-* } 0 }
> diff --git a/libstdc++-v3/testsuite/20_util/move_only_function/call.cc b/libstdc++-v3/testsuite/20_util/move_only_function/call.cc
> index 217de374763..72c8118d716 100644
> --- a/libstdc++-v3/testsuite/20_util/move_only_function/call.cc
> +++ b/libstdc++-v3/testsuite/20_util/move_only_function/call.cc
> @@ -204,13 +204,14 @@ test05()
> }
>
> struct Incomplete;
> +enum CompleteEnum : int;
>
> void
> test_params()
> {
> - std::move_only_function<void(Incomplete)> f1;
> - std::move_only_function<void(Incomplete&)> f2;
> - std::move_only_function<void(Incomplete&&)> f3;
> + std::move_only_function<void(Incomplete&)> f1;
> + std::move_only_function<void(Incomplete&&)> f2;
> + std::move_only_function<void(CompleteEnum)> f4;
> }
>
> int main()
> diff --git a/libstdc++-v3/testsuite/20_util/move_only_function/conv.cc b/libstdc++-v3/testsuite/20_util/move_only_function/conv.cc
> index 3da5e9e90a3..ef8bb37b232 100644
> --- a/libstdc++-v3/testsuite/20_util/move_only_function/conv.cc
> +++ b/libstdc++-v3/testsuite/20_util/move_only_function/conv.cc
> @@ -2,6 +2,7 @@
> // { dg-require-effective-target hosted }
>
> #include <functional>
> +#include <string_view>
> #include <testsuite_hooks.h>
>
> using std::move_only_function;
> @@ -15,6 +16,21 @@ static_assert( !std::is_constructible_v<std::move_only_function<void()&>,
> static_assert( !std::is_constructible_v<std::move_only_function<void() const>,
> std::move_only_function<void()>> );
>
> +using FuncType = int(int);
> +
> +// Top level const qualifiers are ignored in function types, and decay
> +// is performed.
> +static_assert( std::is_same_v<std::move_only_function<void(int const)>,
> + std::move_only_function<void(int)>> );
> +static_assert( std::is_same_v<std::move_only_function<void(int[2])>,
> + std::move_only_function<void(int*)>>);
> +static_assert( std::is_same_v<std::move_only_function<void(int[])>,
> + std::move_only_function<void(int*)>>);
> +static_assert( std::is_same_v<std::move_only_function<void(int const[5])>,
> + std::move_only_function<void(int const*)>>);
> +static_assert( std::is_same_v<std::move_only_function<void(FuncType)>,
> + std::move_only_function<void(FuncType*)>>);
> +
> // Non-trivial args, guarantess that type is not passed by copy
> struct CountedArg
> {
> @@ -177,6 +193,24 @@ test06()
> VERIFY( m1(c) == 2 );
> }
>
> +void
> +test07()
> +{
> + // Scalar types and small trivially move constructible types are passed
> + // by value to invoker. So int&& signature is not compatible for such types.
> + auto fi = [](CountedArg const& arg, int) noexcept { return arg.counter; };
> + std::move_only_function<int(CountedArg, int) const noexcept> mi1(fi);
> + VERIFY( mi1(c, 0) == 1 );
> + std::move_only_function<int(CountedArg, int&&) const noexcept> mi2(std::move(mi1));
> + VERIFY( mi2(c, 0) == 2 );
> +
> + auto fs = [](CountedArg const& arg, std::string_view) noexcept { return arg.counter; };
> + std::move_only_function<int(CountedArg, std::string_view) const noexcept> ms1(fs);
> + VERIFY( ms1(c, "") == 1 );
> + std::move_only_function<int(CountedArg, std::string_view&&) const noexcept> ms2(std::move(ms1));
> + VERIFY( ms2(c, "") == 2 );
> +}
> +
> int main()
> {
> test01();
> @@ -185,4 +219,5 @@ int main()
> test04();
> test05();
> test06();
> + test07();
> }
> diff --git a/libstdc++-v3/testsuite/20_util/move_only_function/incomplete_neg.cc b/libstdc++-v3/testsuite/20_util/move_only_function/incomplete_neg.cc
> new file mode 100644
> index 00000000000..d025c473e28
> --- /dev/null
> +++ b/libstdc++-v3/testsuite/20_util/move_only_function/incomplete_neg.cc
> @@ -0,0 +1,18 @@
> +// { dg-do compile { target c++23 } }
> +
> +#include <functional>
> +
> +struct IncompleteClass;
> +
> +using T1 = std::move_only_function<int(IncompleteClass)>::result_type; // { dg-error "here" }
> +using T2 = std::move_only_function<int(int, IncompleteClass)>::result_type; // { dg-error "here" }
> +
> +enum Enum {
> + x = [] {
> + // Enum enumeration is incomplete here
> + using T3 = std::move_only_function<int(Enum)>::result_type; // { dg-error "here" }
> + return T3(1);
> + }()
> +};
> +
> +// { dg-error "static assertion failed" "" { target *-*-* } 0 }
> --
> 2.49.0
>
More information about the Libstdc++
mailing list