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
70 changes: 70 additions & 0 deletions include/frg/functional.hpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,81 @@
#pragma once

#include <type_traits>
#include <utility>

#include <frg/macros.hpp>

namespace frg FRG_VISIBILITY {

// Non-owning reference to a callable, similar to std::function_ref.
// Useful to pass lambdas across virtual function boundaries without allocating.
// The referenced callable must outlive the function_ref (binding a temporary is fine
// as long as the function_ref is only used within the full expression, e.g., as an argument).
template<typename Signature>
struct function_ref;

template<typename R, typename... Args>
struct function_ref<R(Args...)> {
// Pointers-to-member are excluded since the thunk uses plain call syntax.
template<typename F>
requires (!std::is_same_v<std::remove_cvref_t<F>, function_ref>
&& !std::is_function_v<std::remove_reference_t<F>>
&& !std::is_member_pointer_v<std::remove_cvref_t<F>>
&& std::is_invocable_r_v<R, F &, Args...>)
function_ref(F &&f) noexcept
: thunk_{&invoke_object<std::remove_reference_t<F>>} {
storage_.obj = const_cast<void *>(static_cast<const void *>(__builtin_addressof(f)));
}

template<typename F>
requires (std::is_function_v<F> && std::is_invocable_r_v<R, F *, Args...>)
function_ref(F *f) noexcept
: thunk_{&invoke_function<F>} {
storage_.fn = reinterpret_cast<void (*)()>(f);
}

// Assigning a temporary callable would leave the function_ref dangling
// as soon as the assignment expression ends.
template<typename F>
requires (!std::is_lvalue_reference_v<F>
&& !std::is_same_v<std::remove_cvref_t<F>, function_ref>
&& !std::is_pointer_v<std::remove_cvref_t<F>>)
function_ref &operator= (F &&) = delete;

R operator() (Args... args) const {
return thunk_(storage_, std::forward<Args>(args)...);
}

private:
union storage {
void *obj;
void (*fn)();
};

// The thunks take their arguments by reference to avoid moving
// by-value arguments a second time.
template<typename T>
static R invoke_object(storage s, Args &&... args) {
auto &f = *static_cast<T *>(s.obj);
if constexpr (std::is_void_v<R>)
f(std::forward<Args>(args)...);
else
return f(std::forward<Args>(args)...);
}

template<typename F>
static R invoke_function(storage s, Args &&... args) {
auto f = reinterpret_cast<F *>(s.fn);
if constexpr (std::is_void_v<R>)
f(std::forward<Args>(args)...);
else
return f(std::forward<Args>(args)...);
}

storage storage_;
R (*thunk_)(storage, Args &&...);
};

template<auto Ptr>
struct bound_mem_fn;

Expand Down
87 changes: 87 additions & 0 deletions tests/function_ref.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#include <frg/functional.hpp>

#include <gtest/gtest.h>

#include <memory>

namespace {

int increment(int &x) {
return ++x;
}

} // anonymous namespace

TEST(function_ref, refers_to_callable) {
struct counting {
int operator() () { return ++calls; }
int calls = 0;
};

counting c;
frg::function_ref<int()> fn = c;
fn();
fn();
EXPECT_EQ(c.calls, 2);
}

TEST(function_ref, const_callable) {
struct constant {
int operator() () const { return 42; }
};

const constant c;
frg::function_ref<int()> fn = c;
EXPECT_EQ(fn(), 42);
}

TEST(function_ref, void_return_discards_result) {
int calls = 0;
auto lambda = [&] (int x) { calls++; return x; };
frg::function_ref<void(int)> fn = lambda;
fn(1);
fn(2);
EXPECT_EQ(calls, 2);
}

TEST(function_ref, function_pointer) {
int x = 0;

frg::function_ref<int(int &)> fn = increment;
EXPECT_EQ(fn(x), 1);

frg::function_ref<void(int &)> void_fn = increment;
void_fn(x);
EXPECT_EQ(x, 2);
}

TEST(function_ref, reference_and_move_only_args) {
auto lambda = [] (int &out, std::unique_ptr<int> p) { out = *p; };
frg::function_ref<void(int &, std::unique_ptr<int>)> fn = lambda;

int out = 0;
fn(out, std::make_unique<int>(7));
EXPECT_EQ(out, 7);
}

namespace {

struct member_callable {
int get() { return 0; }
};

struct void_callable {
void operator() () const { }
};

} // anonymous namespace

static_assert(std::is_trivially_copyable_v<frg::function_ref<void()>>);
static_assert(!std::is_constructible_v<frg::function_ref<int(member_callable &)>,
int (member_callable::*)()>);
static_assert(!std::is_assignable_v<frg::function_ref<void()> &, void_callable>);
static_assert(std::is_assignable_v<frg::function_ref<void()> &, void_callable &>);
static_assert(std::is_assignable_v<frg::function_ref<void()> &, void (*)()>);
static_assert(std::is_assignable_v<frg::function_ref<void()> &, const frg::function_ref<void()>>);
static_assert(!std::is_constructible_v<frg::function_ref<void(int)>, int>);
static_assert(!std::is_constructible_v<frg::function_ref<int()>, void (*)()>);
1 change: 1 addition & 0 deletions tests/meson.build
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
gtest = dependency('gtest_main')

test_executable = executable('frigg_tests',
'function_ref.cpp',
'rcu_btree.cpp',
'rcu_radixtree.cpp',
'safe_int.cpp',
Expand Down
Loading