Naive std::function implementation
(889 words) Fri, Apr 8, 2016After exploring std::function
in a previous post, I thought that it might be a good practice to implement a simple (and partial) std::function
. It turned out to be much less code than I anticipated. I hope you’ll like it.
Features
While std::function
has a few typedef
s and methods, the core functionality is assignment and invocation:
- Assignment through
operator=
- Invocation through
operator()
Even though std::function
has a bit more functionality to it, we will only implement the above 2 methods.
Declaration
The standard specifies that std::function
will be declared as follows:
namespace std {
template <typename>
class function; // no definition
template <typename ReturnValue, typename ... Args>
class function<ReturnValue(Args...)> {
// ...
};
}
Why have a std::function
with no definition that is never used? Well, ideally you’d only have the second version. However, that version is a partial template specialization of the first. Another way could have been to define std::function
as:
template <typename ReturnValue, typename ... Args>
class function { ... };
But this would mean that clients would look like std::function<int, bool, float>
rather than std::function<int(bool, float)>
. I personally think that the latter is much nicer, but there’s just no syntax to express this without partial specialization.
So let’s copy that:
template <typename>
class naive_function; // no definition
template <typename ReturnValue, typename ... Args>
class naive_function<ReturnValue(Args...)> {
public:
// operator= goes here
// operator() goes here
private:
...
};
Now any attempt to (mis)use naive_function
with a simple argument list (example: naive_function<bool, int>
) will yield a compiler error along the lines of “using undefined class naive_function”.
Groundwork
Before we move to implement operator=
and operator()
we need to write some supporting code. The following classes will be internal private
to naive_function
, so they know ReturnValue
and Args
.
Let’s start with an interface:
class ICallable {
public:
virtual ~ICallable() = default;
virtual ReturnValue Invoke(Args...) = 0;
};
Easy enough. Now for a concrete implementor:
template <typename T>
class CallableT : public ICallable {
public:
CallableT(const T& t)
: t_(t) {
}
~CallableT() override = default;
ReturnValue Invoke(Args... args) override {
return t_(args...);
}
private:
T t_;
};
Implementation
With the help of the above very simple classes it is now almost trivial to implement naive_function
:
template <typename ReturnValue, typename... Args>
class naive_function<ReturnValue(Args...)> {
public:
template <typename T>
naive_function& operator=(T t) {
callable_ = std::make_unique<CallableT<T>>(t);
return *this;
}
ReturnValue operator()(Args... args) const {
assert(callable_);
return callable_->Invoke(args...);
}
private:
// ICallable as implemented above.
// CallableT as implemented above.
std::unique_ptr<ICallable> callable_;
};
There’s not even a lot of magic here:
operator=
is templated, where T is anything that can be called with Args...
and return ReturnValue
. There we dynamically create a CallableT<T>
which is assigned to callable_
(of type std::unique_ptr<ICallable>
). Now the vtable knows how to execute the proper code at runtime.
operator()
is trivial. It is not allowed to be called before operator=
was called, thus the assert
. After that, simply Invoke
callable_
and return its return-value.
Let’s test our creation:
void func() {
cout << "func" << endl;
}
struct functor {
void operator()() {
cout << "functor" << endl;
}
};
int main() {
naive_function<void()> f;
f = func;
f();
f = functor();
f();
f = []() { cout << "lambda" << endl; };
f();
}
Output:
func
functor
lambda
Future improvements
This implementation lacks a few things which I consider beyond the scope of this post, but feel free to implement them on your own:
Forwarding references and perfect forwarding
Specifically in the following places:
naive_function
’soperator=
naive_function
’soperator()
CallableT
’s constructor
Small-object optimization
For more details about Small Object Optimization (SOO) or Small String
Optimization (SSO) see my previous posts about
std::string
and
std::function
Clang reserves 16 bytes for small objects in order to save dynamic allocations.
In naive_function
we always allocate dynamically.
Special handling for operator=
with naive_function
We have a templated operator=
. Can you guess what happens in the following piece of code?:
naive_function<void()> f;
naive_function<void()> f2;
f2 = f;
I was surprised by this, but this actually fails to compile (tested on Visual Studio and clang). Reason is that copy-assignment-operator is deleted due to the fact that callable_
has no copy-assignment-operator. It does not fallback to our operator=
.
But even if we got it to work, it would create an inefficient double-dereference (or more if this was assigned to yet another naive_function
).
An operator which would copy the internals would save this, and will also behave more sanely when the user changes objects that have been copied.
Appendix: Full code
#include <iostream>
#include <memory>
#include <cassert>
using namespace std;
template <typename T>
class naive_function;
template <typename ReturnValue, typename... Args>
class naive_function<ReturnValue(Args...)> {
public:
template <typename T>
naive_function& operator=(T t) {
callable_ = std::make_unique<CallableT<T>>(t);
return *this;
}
ReturnValue operator()(Args... args) const {
assert(callable_);
return callable_->Invoke(args...);
}
private:
class ICallable {
public:
virtual ~ICallable() = default;
virtual ReturnValue Invoke(Args...) = 0;
};
template <typename T>
class CallableT : public ICallable {
public:
CallableT(const T& t)
: t_(t) {
}
~CallableT() override = default;
ReturnValue Invoke(Args... args) override {
return t_(args...);
}
private:
T t_;
};
std::unique_ptr<ICallable> callable_;
};
void func() {
cout << "func" << endl;
}
struct functor {
void operator()() {
cout << "functor" << endl;
}
};
int main() {
naive_function<void()> f;
f = func;
f();
f = functor();
f();
f = []() { cout << "lambda" << endl; };
f();
}