Identifiable C++ Function Pointer of any kind in Direct-BT (Part 2)

Earlier I started elaborating on Direct-BT C++ implementation details, covering communication channels and data structures suitable for concurrency.

This part shall cover Jaulib’s jau::function<R(A…)>, used to manage callback lists e.g. in BTManager or simply utilizing them to delegate functionality like in jau::service_runner.

Updated on 2023-01-03: Matching new class name and properties.

Function Overview

Similar to std::function, jau::function<R(A…)> stores any callable target function solely described by its return type R and arguments types A... from any source, e.g. free functions, capturing and non-capturing lambda function, member functions.

jau::function<R(A…)> supports equality operations for all func::target_type source types, allowing to manage container of jau::functions, see limitations below.

If a jau::function contains no target, see jau::function<R(A…)>::is_null(), it is empty. Invoking the target of an empty jau::function is a no-operation and has no side effects.

jau::function satisfies the requirements of CopyConstructible, CopyAssignable, MoveConstructible and MoveAssignable.

 

Compared to std::function<R(A...)>, jau::function<R(A...)>

 

Instances of jau::function can store, copy, move and invoke any of its callable targets

 

Implementation Details

jau::function<R(A...)> holds the static polymorphic target function delegate_t<R, A…>,
which itself completely holds up to 24 bytes sized TriviallyCopyable target function objects to avoiding cache misses.

The following table shows the full memory footprint of the target function delegate_t<R, A…> storage,
which equals to jau::function<R(A...)> memory size as it only contains the instance of delegate_t<R, A…>.

Type Signature Target Function Size delegate_t<R, A...> Size Heap Size Total Size TriviallyCopyable
free function<free, void ()> 8 48 0 48 true
member function<member, int (int)> 16 48 0 48 true
lambda_plain function<lambda, int (int)> 24 48 0 48 true
lambda_ref function<lambda, int (int)> 24 48 0 48 true
lambda_copy function<lambda, int (int)> 24 48 0 48 true
ylambda_plain function<ylambda, int (int)> 24 48 0 48 true
capval (small) function<capval, int (int)> 16 48 0 48 true
capval (big) function<capval, int (int)> 48 48 48 96 true
capref function<capref, int (int)> 16 48 0 48 true

Memory sizes are in bytes, data collected on a GNU/Linux arm64 system.

The detailed memory footprint can queried at runtime, see implementation of jau::function<R(A…)>::function::toString().

Static polymorphism is achieved by constructing the delegate_t<R, A…> instance via their func::target_type specific factories, see mapping at func::target_type.

For example the static func::member_target_t::delegate constructs its specific delegate_t<R, A…> by passing its data and required functions to func::delegate_t<R, A…>::make.
The latter is an overloaded template for trivial and non-trivial types and decides which memory is being used, e.g. the internal 24 bytes memory cache or heap if not fitting.

To support lambda identity for the equality operator, jau::type_info is being used either with Runtime Type Information (RTTI) if enabled or using Compile time type information (CTTI), see limitations below.

 

Function Usage

A detailed API usage is covered within test_functional.hpp and test_functional_perf.hpp, see function test00_usage().

Let’s assume we like to bind to the following function prototype bool func(int), which results to jau::function<bool(int)>:

  • Free functions via constructor and jau::bind_free()
  • Class member functions via constructor and jau::bind_member()
  • Lambda functions via constructor
    • Prologue
      int sum = 0;
    • Stateless lambda, equivalent to bool(*)(int) function
      jau::function<bool(int)> func0 = [](int v) -> bool {
      return 0 == v;
      };
    • Stateless by-value capturing lambda
      jau::function<bool(int)> func1 = [sum](int v) -> bool {
      return sum == v;
      };
    • Stateful by-value capturing lambda mutating captured field
      jau::function<bool(int)> func1 = [sum](int v) mutable -> bool {
      sum += v;
      return 0 == v;
      };
    • Stateless by-reference capturing lambda
      jau::function<bool(int)> func1 = [&sum](int v) -> bool {
      sum += v;
      return 0 == v;
      };
    • Stateless by-reference capturing lambda assigning an auto lambda
      auto lambda_func = [&](int v) -> bool {
      sum += v;
      return 0 == v;
      };
      jau::function<bool(int)> func1 = lambda_func;
      jau::function<bool(int)> func2 = lambda_func;
      assert( func1 == func2 );
  • Y combinator and deducing this lambda functions via factory bind_ylambda()
    • Stateless lambda receiving explicit this object parameter reference used for recursion using auto
      function<int(int)> f = function<int(int)>::bind_ylambda( [](auto& self, int x) -> int {
      if( 0 == x ) {
      return 1;
      } else {
      return x * self(x-1); // recursion, calling itself w/o explicitly passing `self`
      }
      } );
      assert( 24 == f(4) ); // `self` is bound to function<int(int)>::delegate_type `f.target`, `x` is 4
    • or using explicit function<R(A...)>::delegate_type
      function<int(int)> f = function<int(int)>::bind_ylambda( [](function<int(int)>::delegate_type& self, int x) -> int {
      if( 0 == x ) {
      return 1;
      } else {
      return x * self(x-1); // recursion, calling itself w/o explicitly passing `self`
      }
      } );
      assert( 24 == f(4) ); // `self` is bound to function<int(int)>::delegate_type `f.target`, `x` is 4
  • Lambda alike capture by-reference to value via constructor and jau::bind_capref()
    • Prologue
      struct big_data {
      int sum;
      };
      big_data data { 0 };
      typedef bool(*cfunc)(big_data*, int); // to force non-capturing lambda into a free function template type deduction
    • Constructor function(I* data_ptr, R(*func)(I*, A…))
      function<int(int)> func(&data,
      (cfunc) ( [](big_data* data, int v) -> bool {
      stats_ptr->sum += v;
      return 0 == v;
      } ) );
    • Factory jau::bind_capref(I* data_ptr, R(*func)(I*, A...))
      jau::function<bool(int)> func = jau::bind_capref(&data,
      (cfunc) ( [](big_data* data, int v) -> bool {
      stats_ptr->sum += v;
      return 0 == v;
      } ) );
  • Lambda alike capture by-copy of value via constructor and jau::bind_capval()
  • std::function function via constructor and jau::bind_std()

Function Limitations

Non unique lambda type names without RTTI using gcc or non clang compiler

Due to limitations of jau::make_ctti<R, L, A…>(), not using RTTI on gcc or non clang compiler will erroneously mistake different lambda functions defined within one function and using same function prototype R<A...> to be the same.

jau::type_info::limited_lambda_id will expose the potential limitation.

See CTTI lambda name limitations and limitations of jau::type_info.