diff --git a/include/psi/functionoid/function_ref.hpp b/include/psi/functionoid/function_ref.hpp index e2ec500..9c4afc5 100644 --- a/include/psi/functionoid/function_ref.hpp +++ b/include/psi/functionoid/function_ref.hpp @@ -33,13 +33,38 @@ class [[ clang::trivial_abi ]] function_ref public: constexpr function_ref() = default; + /// Whether \c make_c_callback stores \c F by value in the data word instead + /// of pointing at the caller's object — i.e. whether this ref borrows at all. + template + static bool constexpr stored_inline + { + std::is_trivially_copy_constructible_v> && + ( sizeof( std::remove_reference_t ) <= sizeof( void * ) ) + }; + + template + static bool constexpr invocable_as + { noexcept( std::declval()( std::declval()... ) ) >= ne }; + + /// Borrowing overload: the target stays where the caller put it, so it must + /// outlive this ref — hence \c lifetimebound. template function_ref( F && callable [[ clang::lifetimebound ]] ) noexcept - requires ( noexcept( callable( std::declval()... ) ) >= ne ) + requires ( invocable_as && !stored_inline ) { - auto const cb{ make_c_callback( std::forward( callable ) ) }; - data_ = cb.first; - function_ = static_cast( cb.second ); + bind( std::forward( callable ) ); + } + + /// Copying overload: a trivially copyable callable that fits the data word is + /// COPIED into this ref, which therefore borrows nothing — so binding a + /// temporary (a lambda built in the argument, a function pointer) is safe and + /// must not be diagnosed. It needs to be a separate overload because + /// `[[clang::lifetimebound]]` cannot be applied conditionally. + template + function_ref( F && callable ) noexcept + requires ( invocable_as && stored_inline ) + { + bind( std::forward( callable ) ); } template @@ -128,6 +153,15 @@ class [[ clang::trivial_abi ]] function_ref } } +private: + template + void bind( F && callable ) noexcept + { + auto const cb{ make_c_callback( std::forward( callable ) ) }; + data_ = cb.first; + function_ = static_cast( cb.second ); + } + private: R ( *function_ )( void *, Args... ) noexcept( ne ){}; void * data_{}; diff --git a/test/function_ref_test.cpp b/test/function_ref_test.cpp index 07880f7..4a9bbfa 100644 --- a/test/function_ref_test.cpp +++ b/test/function_ref_test.cpp @@ -34,3 +34,39 @@ TEST( FunctionRefTest, BoostAliasMatchesPsi ) ref(); EXPECT_EQ( g_value, 43 ); } + +// A callable small and trivial enough to live in the ref's data word is copied +// into it, not pointed at, so it does not outlive-borrow anything: a ref built +// from a temporary stays valid after the full-expression, and may be returned. +TEST( FunctionRefTest, InlineStoredCallableSurvivesItsTemporary ) +{ + int value{ 0 }; + auto const makeRef{ [ &value ]{ + // the lambda is a temporary of this full-expression; it captures one + // pointer, so the ref takes a copy rather than its address + return psi::functionoid::function_ref{ + [ p = &value ]( int const v ) { *p = v; } + }; + } }; + auto const ref{ makeRef() }; + ref( 11 ); + EXPECT_EQ( value, 11 ); +} + +TEST( FunctionRefTest, InlineStorageClassification ) +{ + using Ref = psi::functionoid::function_ref; + + int a{}, b{}; + auto const capturesOnePointer { [ pA = &a ] ( int const v ) { *pA = v; } }; + auto const capturesTwoPointers{ [ pA = &a, pB = &b ]( int const v ) { *pA = *pB = v; } }; + + // copied into the data word -> the ref borrows nothing + static_assert( Ref::stored_inline ); + // too large -> the ref points at the caller's object, which must outlive it + static_assert( !Ref::stored_inline ); + // a plain function pointer is copied too + static_assert( Ref::stored_inline ); + + EXPECT_TRUE( Ref{ capturesTwoPointers } ); // the borrowing overload still works for an lvalue +}