Skip to content

A c++ function closure implementation. It integrates the std::function and std::bind, and even more powerful.

License

Notifications You must be signed in to change notification settings

coyorkdow/closure

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

70 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Closure

Closure is a c++ functional object implementation. It integrates the std::function and std::bind, and even more powerful.

Linux Status macOS Status C++ Standard

Closure is header-only. To use Closure, simply copy the directory include into your project, and add #include "closure/closure.hpp" in your source files.

Features

  • Support almost all the methods of std::function, except target_type() (need RTTI).
  • Support arguments binding, therefore it can replace std::bind.
  • Support range placeholder, PlaceHolder<I, J>() indicates a placeholders sequence from I to J.
  • It can store non-copyable object, like std::unique_ptr. An extra method copyable() is provided to check if the object currently stored in a Closure instance is copyable. If it returns false, then trying copy this instance (construct or assign) will get an empty Closure.
  • Support small object optimization. On x64 machines, any objects of the type which is trivially copyable and sizeof not greater than 16 will be stored locally. No dynamical memory allocated.
  • Helper function MakeClosure can create an instance of Closure and deduce its type, you can use auto instead of manually writing the Closure's template arguments. MakeClosure also supports arguments binding.

Try it online through the Compiler Explorer https://godbolt.org/z/KrGejazf7

Read more details in

Compare to std::function

Compare to std::bind

closure::Any

Basic Usage

Use like std::function. Store a function pointer or any callable object (including pointer to member function).

int calculate_sum(const std::string& exp) {
  int ans = 0;
  int cur_num = 0;
  for (auto iter = exp.begin(); iter < exp.end(); ++iter) {
    if (*iter == '+') {
      assert(iter != exp.begin());
      ans += cur_num;
      cur_num = 0;
    } else {
      assert('0' <= *iter && *iter <= '9');
      cur_num = cur_num * 10 + *iter - '0';
    }
  }
  ans += cur_num;
  return ans;
}

using namespace closure;
Closure<int(const std::string&)> closure1;
closure1 = calculate_sum;
closure1("1+2+3"); // result is 6

std::string exp = "1+2+3";
auto wrap_sum = [=] (const std::string& exp2) {
  return calculate_sum(exp + "+" + exp2);
};
closure1 = wrap_sum;
closure1("4"); // result is 10

Binding

You can fast bind the first n arguments by passing them to the constructor or MakeClosure.

std::size_t sum(const int& v1, double v2, int v3, int v4) noexcept { return v1 + v2 + v3 + v4; }

using namespace closure;
auto closure1 = MakeClosure(sum, 1); // bind 1 to arg v1
// Alternatively, Closure<std::size_t(double, int, int)> closure1(sum, 1);
static_assert(std::is_same<decltype(closure1), Closure<std::size_t(double, int, int)>>::value);
closure1(2, 3, 4); // result is 10

Or, you can use placeholders to process more complicatedly binding. The number of placeholders is unlimited.

// Change the order of arguments.
auto lambda1 = [](int v1, int v2) { return v1 - v2; };

auto closure1 = closure::MakeClosure(lambda1, closure::PlaceHolder<1>(), closure::PlaceHolder<0>());
closure1(5, 3); // result is -2

Or, you can use range placeholder. With range placeholder, not only bind the first n, the last n or middle n is also quite easy. Just use PlaceHolder<I, J>() to make an offset.

auto lambda = [](int a, int b, int c, int d, int e, int f, int g) {
  using std::to_string;
  return to_string(a) + to_string(b) + to_string(c) + to_string(d) + to_string(e) + to_string(f) + to_string(g);
};
auto closure1 = closure::MakeClosure(lambda, closure::PlaceHolder<0, 4>(), 6, 7); // bind the last two arguments
std::string res = closure1(1, 2, 3, 4, 5); // result is "1234567"

Compare to std::function

Stores non-copyable object

std::function can only store the copyable object. Before c++23 introduced std::move_only_function, using only the standard library you cannot create a generic functional type which can hold a non-copyable functor.

class TestClassBindMethod {
 public:
  int ResIntArg1NonConst(int v) { return v; }
};

auto ptr = std::make_unique<TestClassBindMethod>();
auto closure4 = closure::MakeClosure(&TestClassBindMethod::ResIntArg1NonConst, std::move(ptr));
closure4(123); // result is 123
auto bounded = [capture0 = std::make_unique<TestClassBindMethod>()](int v) {
  return capture0->ResIntArg1NonConst(v);
};
//  std::function<int(int)> _ = std::move(bounded);  // can't compile
closure4 = std::move(bounded);

assert(!closure4.copyable()); // cannot copy a std::unique_ptr
auto closure5 = closure4;
assert(!closure5); // trying copy a non-copyable closure will get an empty result.

Auto deduction

Before c++17, when using std::function you have to correctly write the complete type of the object you want to construct. MakeClosure can help you omit this step.

template <class C, class... Args>
auto MakeClosure(C&&, Args&&...); // deduce the return type

If C is a function pointer, or a member function pointer (a.k.a. pointer to class method), or a "simple functor". Then MakeClosure can be applied, and it will return a Closure instance with the proper type.

A simple functor is a non-template, non-generic lambda, or a class type with one and only one operator() overloading, while this operator() is not a template. More formly, a class F is a functor if and only if decltype(&F::operator()) is a valid expression, and it is a type of member function pointer.

These rules are similar to the deduction guides of std::function that introduced in c++17. However, the deduction rules don't contain the deduction for member function pointers, moreover, MakeClosure doesn't need c++17.

struct NonSimple {
  std::string operator()() const { return "empty"; }
  int operator()(int a, int b) const { return a + b; }
};
//  MakeClosure(NonSimple{}); // can't compile
closure::Closure<std::string()> closure1 = NonSimple{};
closure1(); // result is "empty";
Closure<int(int, int)> closure2 = NonSimple{};
closure2(1, 2); // result is 3

struct Simple {
  int operator()(int a, int b) const { return a + b; }
};
closure2 = MakeClosure(Simple{}); // ok

Compare to std::bind

std::bind is somehow a bad design and is considered to be deprecated. But Closure makes up many drawbacks of std::bind.

Type specified

The type of the object return from std::bind is unspecified, which means you have to store it in the std::function to save it elsewhere. Closure integrates the arguments binding, and each Closure instance has a determined type.

Range binding

std::bind cannot bind first n arguments directly. Instead, you have to use std::placeholders::_1, std::placeholders::_2, ... in order. The standard didn't introduce std::bind_front until c++20, and std::bind_back until c++23. Closure provides such feature that you can simply bind first n arguments, same as bind_front.

Besides, with the range placeholder. closure can even offer a more flexible range binding than just bind front or bind back.

No error binding

Using std::bind you can even create a "callable" object that cannot call at all. Later when you try to call it, IDE and compiler will give you a lot of errors that hard to read. But using Closure you can never create a closure that unable to call. And the error messages are more human-friendly because it's incurred by a static_assert.

auto lambda = [](std::unique_ptr<int>) {};
auto b = std::bind(lambda, std::make_unique<int>()); // you can create b, even if it's wrong from the beginning.
b(); // the compiler will only complain error when you try to call it.
closure::MakeClosure(lambda, std::make_unique<int>()); // error, and the compiler will give an error message that easy to read.

If you use std::bind, when b(), the error message given by the gcc maybe very long, for example like (only a small part of the front, since it's too long)

: error: no match for call to '(std::_Bind<TestClosureWithPlaceHolders_Method_Test::TestBody()::<lambda(std::unique_ptr<int>)>(std::unique_ptr<int>)>) ()'
  620 |   b();
      |   ~^~
In file included from googletest/include/gtest/gtest-printers.h:104,
                 from googletest/include/gtest/gtest-matchers.h:48,
                 from googletest/include/gtest/internal/gtest-death-test-internal.h:46,
                 from googletest/include/gtest/gtest-death-test.h:43,
                 from googletest/include/gtest/gtest.h:61,
                 from :
/usr/local/Cellar/gcc/12.2.0/include/c++/12/functional:565:9: note: candidate: 'template<class ... _Args, class _Result> _Result std::_Bind<_Functor(_Bound_args ...)>::operator()(_Args&& ...) [with _Args = {_Args ...}; _Functor = TestClosureWithPlaceHolders_Method_Test::TestBody()::<lambda(std::unique_ptr<int>)>; _Bound_args = {std::unique_ptr<int, std::default_delete<int> >}]'
  565 |         operator()(_Args&&... __args)
      |         ^~~~~~~~
/usr/local/Cellar/gcc/12.2.0/include/c++/12/functional:565:9: note:   template argument deduction/substitution failed:
/usr/local/Cellar/gcc/12.2.0/include/c++/12/functional: In substitution of 'template<class _Functor, class ... _Bound_args> template<class _Fn, class _CallArgs, class ... _BArgs> using _Res_type_impl = typename std::result_of<_Fn&(decltype (std::_Mu<typename std::remove_cv<_BArgs>::type, std::is_bind_expression<typename std::remove_cv<_BArgs>::type>::value, (std::is_placeholder<typename std::remove_cv<_BArgs>::type>::value > 0)>()(declval<_BArgs&>(), declval<_CallArgs&>()))&& ...)>::type [with _Fn = TestClosureWithPlaceHolders_Method_Test::TestBody()::<lambda(std::unique_ptr<int>)>; _CallArgs = std::tuple<>; _BArgs = {std::unique_ptr<int, std::default_delete<int> >}; _Functor = TestClosureWithPlaceHolders_Method_Test::TestBody()::<lambda(std::unique_ptr<int>)>; _Bound_args = {std::unique_ptr<int, std::default_delete<int> >}]':
/usr/local/Cellar/gcc/12.2.0/include/c++/12/functional:532:8:   required by substitution of 'template<class _Functor, class ... _Bound_args> template<class _CallArgs> using _Res_type = std::_Bind<_Functor(_Bound_args ...)>::_Res_type_impl<_Functor, _CallArgs, _Bound_args ...> [with _CallArgs = std::tuple<>; _Functor = TestClosureWithPlaceHolders_Method_Test::TestBody()::<lambda(std::unique_ptr<int>)>; _Bound_args = {std::unique_ptr<int, std::default_delete<int> >}]'
/usr/local/Cellar/gcc/12.2.0/include/c++/12/functional:562:9:   required from here
/usr/local/Cellar/gcc/12.2.0/include/c++/12/functional:528:15: error: no type named 'type' in 'struct std::result_of<TestClosureWithPlaceHolders_Method_Test::TestBody()::<lambda(std::unique_ptr<int>)>&(std::unique_ptr<int>&)>'
  528 |         using _Res_type_impl
      |               ^~~~~~~~~~~~~~

But the error message caused by closure::MakeClosure(lambda, std::make_unique<int>()) is much shorter, and will tell you "the given arguments don't match the arguments of callee".

In file included from :
closure.hpp: In instantiation of 'class closure::closureimpl::ClosureImpl<void(std::unique_ptr<int>), TestClosureWithPlaceHolders_Method_Test::TestBody()::<lambda(std::unique_ptr<int>)>, closure::ArgList<std::unique_ptr<int, std::default_delete<int> > >, void>':
/usr/local/Cellar/gcc/12.2.0/include/c++/12/type_traits:734:38:   required from 'struct std::is_trivially_copyable<closure::closureimpl::ClosureImpl<void(std::unique_ptr<int>), TestClosureWithPlaceHolders_Method_Test::TestBody()::<lambda(std::unique_ptr<int>)>, closure::ArgList<std::unique_ptr<int, std::default_delete<int> > >, void> >'
closure.hpp:201:68:   required from 'struct closure::closureimpl::soo::IsSmallObject<closure::closureimpl::ClosureImpl<void(std::unique_ptr<int>), TestClosureWithPlaceHolders_Method_Test::TestBody()::<lambda(std::unique_ptr<int>)>, closure::ArgList<std::unique_ptr<int, std::default_delete<int> > >, void> >'
closure.hpp:216:80:   required by substitution of 'template<class Tp, class ... Args, typename std::enable_if<(! closure::closureimpl::soo::IsSmallObject<Tp>::value), int>::type <anonymous> > void closure::closureimpl::StoragePool::emplace(Args&& ...) [with Tp = closure::closureimpl::ClosureImpl<void(std::unique_ptr<int>), TestClosureWithPlaceHolders_Method_Test::TestBody()::<lambda(std::unique_ptr<int>)>, closure::ArgList<std::unique_ptr<int, std::default_delete<int> > >, void>; Args = {TestClosureWithPlaceHolders_Method_Test::TestBody()::<lambda(std::unique_ptr<int, std::default_delete<int> >)>&, std::unique_ptr<int, std::default_delete<int> >}; typename std::enable_if<(! closure::closureimpl::soo::IsSmallObject<Tp>::value), int>::type <anonymous> = <missing>]'
closure.hpp:312:35:   required from 'auto closure::closureimpl::MakeClosureImpl(StoragePool*, closure::ArgList<Tps2 ...>, Callable&&, Bounds&& ...) [with R = void; ClosureArgs = {std::unique_ptr<int, std::default_delete<int> >}; Callable = TestClosureWithPlaceHolders_Method_Test::TestBody()::<lambda(std::unique_ptr<int>)>&; Bounds = {std::unique_ptr<int, std::default_delete<int> >}; typename std::enable_if<(! closure::placeholders::HasPlaceHolder<closure::ArgList<Os2 ...> >::value), int>::type <anonymous> = 0]'
closure.hpp:500:113:   required from 'auto closure::MakeClosure(Functor&&, Bounds&& ...) [with Functor = TestClosureWithPlaceHolders_Method_Test::TestBody()::<lambda(std::unique_ptr<int>)>&; Bounds = {std::unique_ptr<int, std::default_delete<int> >}; typename std::enable_if<(traits::IsSimpleFunctor<typename std::remove_reference<_Tp>::type>::value && (! placeholders::HasPlaceHolder<ArgList<Tps2 ...> >::value)), int>::type <anonymous> = 0]'
:   required from here
closure.hpp:111:28: error: static assertion failed: the given arguments don't match the arguments of callee
  111 |   static_assert(validator::is_invokable, "the given arguments don't match the arguments of callee");
      |                            ^~~~~~~~~~~~
closure.hpp:111:28: note: 'closure::closureimpl::Validator<TestClosureWithPlaceHolders_Method_Test::TestBody()::<lambda(std::unique_ptr<int>)>, closure::ArgList<std::unique_ptr<int, std::default_delete<int> > >, closure::ArgList<std::unique_ptr<int, std::default_delete<int> > > >::is_invokable' evaluates to false

Better SOO

Small object optimization (SOO) for std::function is well supported in gcc and other well known compilers. An object applicable SOO will be stored locally, and no more dynamic memory allocating. But SOO has some restrictions. For example, in gcc, only the trivially-copyable objects can be locally-stored. Meanwhile, there is no guaranteed that the return type of the std::bind is trivially-copyable, even if the callable object and all the binding arguments are trivially-copyable.

auto bad_bind = std::bind([](int a, int b) { return a + b; }, 1, std::placeholders::_1);
static_assert(std::is_trivially_copyable<decltype(bad_bind)>::value, ""); // error, bad_bind is not a trivially copyable object.

In Closure, any callable object (and its binding arguments) is enclosed in an internal type called ClosureImpl. If a ClosureImpl type satisfies closure::closureimpl::soo::IsSmallObject, Then the corresponding closure will be stored locally.

It is guaranteed that if 1) the callable object's type is trivially-copyable and 2) for each binding arguments the type of argument is trivially-copyable (placeholders are all trivially-copyable), then the ClosureImpl is trivially-copyable.

// The underlying `ClosureImpl` type is trivially-copyable. 
auto closure = MakeClosure([](int a, int b) { return a + b; }, 1);

closure::Any

Sometimes you may create a closure with the discontinuous placeholders, thus some parameters are useless and will be abandoned when calling. For example. Object c takes 4 arguments, but only the 2nd and 4th arguments are meaningful. As for the 1st and 3rd arguments, they can be anything.

auto lambda = [](int a, int b) { return a + b; };
auto c = MakeClosure(lambda, PlaceHolder<1>(), PlaceHolder<3>());

closure("123", 4, "567", 8); // ok, result is 12
closure(std::vector<int>{1, 2}, 3, std::vector<long>{4, 5}, 6); // ok, result is 9

When the placeholders are discontinuous, MakeClosure will let closure::Any be the type of the useless parameters. So the type of c is closure::Closure<int(closure::Any, int, closure::Any, int).

std::is_same<decltype(c), closure::Closure<int(closure::Any, int, closure::Any, int)>>::value; // equals to true

Since closure::Any means any type, you can assign c to any other closure with the type that the useful parameters are matched. Like

// it's ok that the 4th parameter is float type, because float can implicitly convert to int
Closure<int(int, int, std::string, float)> c2(lambda, PlaceHolder<1>(), PlaceHolder<3>());
c2 = c;
c2(1, 2, "3", 4.2); // result is 6

About

A c++ function closure implementation. It integrates the std::function and std::bind, and even more powerful.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published