8 min. read

First I want to say thanks to this channel:

Weekly with Jason Turner

Improving C++ knowledge

This channel promotes advanced learning materials on C++,
I’ve persnoally been in a lecture by Jason and it was one of my best learning experiences.


Preface

Binding functions and using delegates in a static compiled manner is no easy task.

Modern C++ moves from void* semantics to more concrete types which make it possible to bind various functionality including lambdas.

In this post we’ll cover 3 basic lessons to start building our knowledge.

Why do we need functions and bindings?

To understand these lessons we’ll need to ask ourselves what kind of goals are we trying to achieve.

Callback mechanism

Async code and event-base mechanisms need callbacks.

A Callback as a concept is the action that happens when something is done.
For example when a popcorn being made is done we grab popcorn.

In psuedo code:

1
2
3
4
5
TakeAndEat(Popcorn) { NomNomNom();  }
MakePopcorn(Popcorn, WhatToDoAfterDone) { }

// Calling the action
MakePopcorn(Popcorn, TakeAndEat)

Lesson one - std::function, std::bind and std::invoke

These 3 functionalities are the basics.
To read more about them in the C++ Reference library:

C++ Reference - std::invoke
C++ Reference - std::function
C++ Reference - std::bind

Callable

A callable is something that can be invoked,
What is invoke or invokable?

The C++ Reference tells us that INVOKE should be valid:
INVOKE<R>(f, std::declval<ArgTypes>()...)

  • Can be called.
  • Has return type of R.
  • f suits type T which can be called.
  • ArgTypes complete for the arguments.

What usually we call?

  • Functions.
  • Member functions.
  • Lambdas.
  • Callable objects (operator() implementation)

Callable objects

Callable objects are objects which can be called.
The operator() overload makes the object itself to invoke a functionality just like a function delegate or a function.

1
2
3
4
5
6
7
8
9
10
11
12
struct AdderPrinter
{
void operator()(int a, int b)
{
int c = a + b;
std::cout << "Result is : " << c << "\n";
}
};

AdderPrinter ap;

ap(1,2);

Adding the Ctor this is how lambdas work.
By capturing the parameters beforehand we can create an instance we can use, copy and move.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct AdderPrinter
{
AdderPrinter(int a,int b) :mA(a),mB(b) {}

void operator()()
{
int c = mA + mB;
std::cout << "Result is : " << c << "\n";
}
private:
int mA=0;
int mB=0;
};

AdderPrinter ap{1,2};

ap();

Invoke

Invokes a Callable.
The most simple explenation is that the invoke is able to differentiate between the different callables - accept them and invoke the function - either itself, or the operator ().

Example:

1
2
3
4
5
6
7
8
9
10
11
void SayHi()
{
std::cout << "Hi\n";
}

void SayHiTo(std::string name)
{
std::cout << "Hello " << name << "\n";
}
std::invoke(SayHi);
std::invoke(SayHiTo,"Bob");

Function

A type that wraps a function - a state we can hold in class as a state.
This allows us to create a more type safe delegates.

1
2
3
4
5
6
7
8
void SayHi()
{
std::cout << "Hi\n";
}

std::function<void(void)> func{ SayHi };

func();

Bind

Bounding a function to arguments, it allows us to create partial callables or rearrange the parameters.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<class T>
auto Add(T a, T b) -> decltype(a + b)
{
return a + b;
}

using namespace std::placeholders;
// Note the usage of _1 to bind the location
auto AddWith5 = std::bind(Add<int>, 5, _1);
auto res = AddWith5(10);
std::cout << res << "\n";

auto AddRearranges = std::bind(Add<int>, _2, _1);
auto res2 = AddRearranges(3, 2)
std::cout << res2 << "\n";

Lesson two - Variadic templates

Any implementation of bind, function, invoke involves getting arguments of various types.

I’ll reference C# implementation of generics and its in ability of variadic templates and why we need them, well… the docs will talk themselves:

C# Action

I’ll just say it - we don’t like that.

Instead C++ generics implements it in a static compiled type manner.
Means the types should be known in compile time (There are some exceptions but I won’t get into that).

The goal is to be able to implement a bind with different types:

1
InvokeMyFunc(Func,1,"Mah String", 0.2f, std::string("BBBbye"));

Obviosuly implementing like that is possible:

1
2
3
4
5
6
7
template<class Callable, class Tone>
InvokeMyFunc(Callable , Tone one);
template<class Callable, class Tone, class Ttwo>
InvokeMyFunc(Callable , Tone one, Ttwo two);
// ...
template<class Callable, class Tone, class Ttwo,class Tthree, class TFour, class TFive, class TSix /*and on and on .... */>
InvokeMyFunc(Callable , Tone one, Ttwo two /* ... */ TSixteen);

Variadic templates allow us to pack and unpack arguments.
This allow us to create multiple parameter functions very easy:

In C++ 17 you can use the fold expressions to allow it to be written like that:

1
2
3
4
5
template<typename... TArgs>
auto Add(TArgs... args)
{
return (args + ...);
}

Before C++ 17 you’ll need a template closing method:

1
2
3
4
5
6
7
8
9
10
11
template<class TOne>
auto Add(TOne one)
{
return one;
}

template<class TOne, class... T>
auto Add(TOne one, T... types)
{
return one + Add(types...);
}

So the Invokeable is can easily be declared variadic for the arguments:

1
2
template<class Callable,class... TArgs >
InvokeMyFunc(Callable callable, TArgs args);

Lesson three - Implementing a basic function wrapper

This Function will hold a simple pointer to a function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<class TReturn, class... T>
class Function
{
public:
using Func = TReturn(T...);

Function(Func* func)
{
mFunc = func;
}

auto operator()(T... t) -> TReturn
{
return std::invoke(mFunc, t...);
}


private:
Func* mFunc;
};

We can use it with a return type, arguments and lambdas.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void Hello()
{
std::cout << "Hello World\n";
}

std::string Format(std::string person)
{
using namespace std::string_literals;
return "Hello "s + person;
}

Function<void> f{ Hello };
Function<void> f2{ []() {std::cout << "Goodbye\n"; } };

Function<std::string, std::string> f4{ Format };
f();
f2();
auto formatted = f4("Bob");

Using a lambda capture we can capture member functions:

1
2
3
A a;
Function<void, A&> f5{ [](A& a) { a.SayHi(); } };
f5(a);

This isn’t the most sophisticated implementation.
We can deal it with std::bind which we’ll dive deeper into it in the next part.

C++ 20 introduced std::mem_fn for function callables of member functions!

In the next post we’ll learn more about the binding mechanism and dive deeper into forward, decay and semantics.

Thanks for reading!


Is this helpful? You can show your appreciation here: Buy me a coffee