The C++ language standard library comes with a std::function
template type which represents a “thing you can invoke”. It can hold any callable, such as
- Function pointer.
- Lambda.
- Other object with
operator()
.
The way this is done is with the assistance of a polymorphic helper object that understands the specific callable it is wrapping.
Here’s a sketch. For concreteness, let’s say we’re implementing std::function<bool(int, char*)>
. For readability, I’ve de-uglified¹ the identifiers.
struct callable_base { callable_base() = default; virtual ~callable_base() { } virtual bool invoke(int, char*) = 0; virtual unique_ptr<callable_base> clone() = 0; }; template<typename T> struct callable : callable_base { T m_t; callable(T const& t) : m_t(t) {} callable(T&& t) : m_t(move(t)) {} bool invoke(int a, char* b) override { return m_t(a, b); } unique_ptr<callable_base> clone() override { return make_unique<callable>(m_t); } }; struct function { std::unique_ptr<callable_base> m_callable; template<typename T> function(T&& t) : m_callable(new callable<decay_t<T>> (forward<T>(t))) { } function(const function& other) : m_callable(other.m_callable ? other.m_callable->clone() : nullptr) { } function(function&& other) = default; bool operator()(int a, char* b) { // TODO: bad_function_call exception return m_callable->invoke(a, b); } };
The idea is that each function
has a callable_base
, which is an interface that allows us to perform basic operations on callable objects: Create a copy, invoke it, and destroy it. Invoking the function
forwards the invoke to the callable_base
. Copying the function
requires a special clone
method on the callable_base
, because unique_ptr
is not copyable.
Constructing the function
is a matter of creating a custom callable
for the specific functor. It’s conceptually simple, but the C++ language makes us write out a bunch of stuff to get it to work. We just want a callable that wraps the thing that was passed to the constructor.
The std::function
in the standard library is basically like this, but with additional optimizations to avoid an allocation in the case of a small callable
. Said optimizations are in fact mandatory by the standard if the callable is a plain function pointer or a reference_wrapper
.
We’ll look at that optimization next time, because it gives us some insight into how we can do similar things with our own types.
¹ Uglification is the process of taking readable names and transforming them into names that are reserved for the implementation. Different libraries have different uglification conventions. For the Microsoft Visual C++ implementation of the standard library, the uglifications tend to be
_My
prefix for member variables._Ty
prefix for type names._Fn
prefix for functors._P
prefix for pointers._
(and capital first letter) for most other things.
I think this is also how
std::any
can work without RTTI. Sy Brand gave a talk about this way of doing runtime polymorphism at Pure Virtual C++ Conference. His talk illustrates the fact that when you have a good framework for static polymorphism and metaprogramming in a language, efficient dynamic polymorphism becomes very easy to implement as a library. Either of which I still do not see the sign of coming to C# at any time soon.Uglification: when you have printf, which calls _vfprintf_l (what’s _l?), which calls __stdio_common_vfprintf (where did _l go?).
I don’t find it to be ugly.
The _l version is a bonus function that takes a locale parameter. Without looking at the code, I would imagine printf just calls a “getcurrentlocale()” style function and passes that along as the locale. The final function is the actual implementation and not part of the public API. Could printf call __stdio_common_vfprintf directly? Sure but it means more maintenance work if __stdio_common_vfprintf gains a new flag etc.