Open-Source Internship opportunity by OpenGenus for programmers. Apply now.
First, I introduce you to the std::function and then proceed to the implementation
Class template std::function is a general-purpose polymorphic function wrapper. Instances of std::function can store, copy, and invoke any CopyConstructible Callable target:
- functions
- lambda expressions
- bind expressions
- pointers to member functions
- pointers to data members.
- or other function objects
std::function is introduced in C++ 17
The stored callable object is called the target of std::function. If a std::function contains no target, it is called empty. Invoking the target of an empty std::function results in std::bad_function_call exception being thrown.
For using std::function you need to
#include <functional>
List of different ways and types that you can store in std::function:
Functions
void print_num(int i)
{
std::cout << i << '\n';
}
int main()
{
// store a function via function pointer
std::function<void(int)> func = print_num;
func(-9);
}
>> -9
Lambda expressions
int main()
{
// store a lambda
std::function<void()> func = []() { std::cout << 30 << '\n'; };
func();
}
>> 30
Bind expressions
void print_num(int i)
{
std::cout << i << '\n';
}
int main()
{
// store the result of a call to std::bind
std::function<void()> func = std::bind(print_num, 31337);
func();
// you also can use std::bind with class objects
}
>> 31337
Pointers to member functions
struct Foo {
Foo(int number) : num(number) {}
void print_add(int i) const
{ std::cout << num + i << '\n'; }
int num;
};
int main()
{
// store a call to a member function
std::function<void(const Foo&, int)> func = &Foo::print_add;
const Foo foo(314159);
func(foo, 1);
}
>> 314160
Pointers to data members
struct Foo {
Foo(int number) : num(number) {}
void print_add(int i) const
{ std::cout << num + i << '\n'; }
int num;
};
int main()
{
// store a call to a data member accessor
std::function<int(Foo const&)> func = &Foo::num;
const Foo foo(314159);
std::cout << "num: " << func(foo) << '\n';
}
>> num: 314159
Other function objects
struct PrintNum {
void operator()(int i) const
{
std::cout << i << '\n';
}
};
int main()
{
// store a call to a function object
std::function<void(int)> func = PrintNum();
func(18);
//and others
>> 18
std::function is cool BUT !!!
If you check the performance using a pure function pointer and std::function. A pure function pointer is much faster in almost all cases. When using std::function, the compiler cannot optimize the function call, because std::function encapsulates it.
Also, sometimes a std::function can allocate memory on the heap, this happens when the callable object is too large. Therefore, you should also think that the copy construct can also allocate memory
std::function can avoid allocations by using a small bit of stack space in each object
- Small buffer optimisation (the same principle as std::string)
- If a callable thing is bigger than the stack space then std::function will get some memory from the heap
- The size of the stack space is implementation defined
So think about whether you really need it. Of course, if you need to keep some functions and classes together, it is extrimly useful
Implementation
Below are examples of std-function implementation. There is also a way to implement without inheritance, but it is very confusing and shows itself worse in benchmarks compared to other implementations. You can find the full code at the link at the end of the page
ALL BENCHMARKS ARE LISTED UNDER THE CODE
Define a template class, then pass the function signature you want into that template parameter
As I said earlier std::function use a little buffer optimization, so for the first time let's try to implement something like that.
// we need these headers
#include <memory>
#include <array>
template <typename>
class function;
template <typename Result, typename... Arguments>
class function<Result (Arguments...)>
{
// ...
}
So how we need to store out callable object? All our classes must contain different classes, for example, a pointer to a function, a lambda function (remember that for each lambda function a new class is created), or others.
Therefore, we create a FunctorHolderBase class that can contain any Callable types using Polymorphism. Then we create a FunctorHolder template type for each new argument type that is passed to our std::function implementation.
So, as a result, our class stores a pointer to our FunctorHolderBase class.
template <typename ReturnType, typename... Args>
struct FunctorHolderBase
{
virtual ~FunctorHolderBase() {}
virtual ReturnType operator()(Args&&...) = 0;
virtual void copyInto (void*) const = 0;
virtual FunctorHolderBase<Result, Arguments...>* clone() const = 0;
};
template <typename Functor, typename ReturnType, typename... Args>
struct FunctorHolder final : FunctorHolderBase<Result, Arguments...>
{
FunctorHolder (Functor func) : f (func) {}
ReturnType operator()(Args&&... args) override
{
return f (std::forward<Arguments> (args)...);
}
void copyInto (void* destination) const override
{
new (destination) FunctorHolder (f);
}
FunctorHolderBase<Result, Arguments...>* clone() const override
{
return new FunctorHolder (f);
}
Functor f;
};
FunctorHolderBase<Result, Arguments...>* functorHolderPtr = nullptr;
So in our construct we check if we can save our template value in stack memory. If so we store it on stack otherwise we store it on heap.
typename std::aligned_storage<32>::type stack;
template <typename Functor>
function (Functor f)
{
if (sizeof (FunctorHolder<Functor, Result, Arguments...>) <= sizeof (stack))
{
functorHolderPtr = (decltype (functorHolderPtr)) std::addressof (stack);
new (functorHolderPtr) FunctorHolder<Functor, Result, Arguments...> (f);
}
else
{
functorHolderPtr = new FunctorHolder<Functor, Result, Arguments...> (f);
}
}
Other code that we needed
Copy constructor, destructor, overload assign operator, overload ()operator
function (const function& other)
{
if (other.functorHolderPtr != nullptr)
{
if (other.functorHolderPtr == (decltype (other.functorHolderPtr)) std::addressof (other.stack))
{
functorHolderPtr = (decltype (functorHolderPtr)) std::addressof (stack);
other.functorHolderPtr->copyInto (functorHolderPtr);
}
else
{
functorHolderPtr = other.functorHolderPtr->clone();
}
}
}
function& operator= (function const& other)
{
if (functorHolderPtr != nullptr)
{
if (functorHolderPtr == (decltype (functorHolderPtr)) std::addressof (stack))
functorHolderPtr->~FunctorHolderBase();
else
delete functorHolderPtr;
functorHolderPtr = nullptr;
}
if (other.functorHolderPtr != nullptr)
{
if (other.functorHolderPtr == (decltype (other.functorHolderPtr)) std::addressof (other.stack))
{
functorHolderPtr = (decltype (functorHolderPtr)) std::addressof (stack);
other.functorHolderPtr->copyInto (functorHolderPtr);
}
else
{
functorHolderPtr = other.functorHolderPtr->clone();
}
}
return *this;
}
function() = default;
~function()
{
if (functorHolderPtr == (decltype (functorHolderPtr)) std::addressof (stack))
functorHolderPtr->~FunctorHolderBase();
else
delete functorHolderPtr;
}
Result operator() (Arguments&&... args) const
{
return (*functorHolderPtr) (std::forward<Arguments> (args)...);
}
So using this implementation we have this benchmark result
Can we do better?
Only heap allocated ?
Also I check the method that allocates only to the heap.
Here are the method that are different in this variant
template <typename Functor>
function (Functor f)
: functorHolderPtr (new FunctorHolder<Functor, Result, Arguments...> (f))
{}
function (const function& other)
{
if (other.functorHolderPtr != nullptr)
functorHolderPtr = other.functorHolderPtr->clone();
}
function& operator= (function const& other)
{
delete functorHolderPtr;
if (other.functorHolderPtr != nullptr)
functorHolderPtr = other.functorHolderPtr->clone();
return *this;
}
~function()
{
delete functorHolderPtr;
}
Only stack allocated ?
And I check the method that allocates only to the stack.
Here are the method that are different in this variant
template <typename Functor>
function (Functor f)
{
static_assert (sizeof (FunctorHolder<Functor, Result, Arguments...>) <= sizeof (stack), "Too big!");
functorHolderPtr = (FunctorHolderBase<Result, Arguments...>*) std::addressof (stack);
new (functorHolderPtr) FunctorHolder<Functor, Result, Arguments...> (f);
}
function (const function& other)
{
if (other.functorHolderPtr != nullptr)
{
functorHolderPtr = (FunctorHolderBase<Result, Arguments...>*) std::addressof (stack);
other.functorHolderPtr->copyInto (functorHolderPtr);
}
}
function& operator= (function const& other)
{
if (functorHolderPtr != nullptr)
{
functorHolderPtr->~FunctorHolderBase<Result, Arguments...>();
functorHolderPtr = nullptr;
}
if (other.functorHolderPtr != nullptr)
{
functorHolderPtr = (FunctorHolderBase<Result, Arguments...>*) std::addressof (stack);
other.functorHolderPtr->copyInto (functorHolderPtr);
}
return *this;
}
~function()
{
if (functorHolderPtr != nullptr)
functorHolderPtr->~FunctorHolderBase<Result, Arguments...>();
}
Benchmarks
Non type erased
As you can see in the benchmark histogram, there is an extremely fast implementation of std::function, so what does it look like. It's very simple, it's a simple pointer to a function:
template <typename>
class function;
template <typename Result, typename... Arguments>
class function<Result (Arguments...)>
{
public:
template <typename Functor>
function (Functor f)
: functionPtr (f)
{}
function() = default;
Result operator() (Arguments&&... args) const
{
return functionPtr (std::forward<Arguments> (args)...);
}
Result(*functionPtr)(Arguments...) = nullptr;
};
As I said, it's really just a function pointer. And if you use this approach, the compiler can optimize some things, but if we use the type erasure method, it becomes very difficult for the compiler to do optimizations with this
With this article at OpenGenus, you must have the complete idea about std::function in C++. Enjoy.