Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 38 additions & 4 deletions include/psi/functionoid/function_ref.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,38 @@ class [[ clang::trivial_abi ]] function_ref<R( Args... ) noexcept( ne )>
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 <typename F>
static bool constexpr stored_inline
{
std::is_trivially_copy_constructible_v<std::remove_reference_t<F>> &&
( sizeof( std::remove_reference_t<F> ) <= sizeof( void * ) )
};

template <typename F>
static bool constexpr invocable_as
{ noexcept( std::declval<F &>()( std::declval<Args>()... ) ) >= ne };

/// Borrowing overload: the target stays where the caller put it, so it must
/// outlive this ref — hence \c lifetimebound.
template <typename F>
function_ref( F && callable [[ clang::lifetimebound ]] ) noexcept
requires ( noexcept( callable( std::declval<Args>()... ) ) >= ne )
requires ( invocable_as<F> && !stored_inline<F> )
{
auto const cb{ make_c_callback( std::forward<F>( callable ) ) };
data_ = cb.first;
function_ = static_cast<decltype( function_ )>( cb.second );
bind( std::forward<F>( 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 <typename F>
function_ref( F && callable ) noexcept
requires ( invocable_as<F> && stored_inline<F> )
{
bind( std::forward<F>( callable ) );
}

template <typename... CallArgs>
Expand Down Expand Up @@ -128,6 +153,15 @@ class [[ clang::trivial_abi ]] function_ref<R( Args... ) noexcept( ne )>
}
}

private:
template <typename F>
void bind( F && callable ) noexcept
{
auto const cb{ make_c_callback( std::forward<F>( callable ) ) };
data_ = cb.first;
function_ = static_cast<decltype( function_ )>( cb.second );
}

private:
R ( *function_ )( void *, Args... ) noexcept( ne ){};
void * data_{};
Expand Down
36 changes: 36 additions & 0 deletions test/function_ref_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<void( int )>{
[ 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<void( int )>;

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<decltype( capturesOnePointer )> );
// too large -> the ref points at the caller's object, which must outlive it
static_assert( !Ref::stored_inline<decltype( capturesTwoPointers )> );
// a plain function pointer is copied too
static_assert( Ref::stored_inline<void (*)( int )> );

EXPECT_TRUE( Ref{ capturesTwoPointers } ); // the borrowing overload still works for an lvalue
}
Loading