I wrote my own "slot" aka "callable wrapper" because I wanted to provide member function slot rebinding on other objects (i.e. I needed a way to store the member function pointer and a pointer to the class in question).
I ran a small size test and discovered std::function
on my system (64-bit Linux) was twice (GCC/libstdc++) to three times (Clang/libc++) the size of my own implementation of a similar class, with a size of 16 bytes. The implementation for non-member functions and lambda's goes like this (the const void*
first argument is for uniformity with member function slots not shown here):
template<typename... ArgTypes>
class slot
{
public:
virtual ~slot() = default;
virtual void operator()(const void* object, ArgTypes...) const = 0;
protected:
slot() = default;
};
template<typename Callable, typename... ArgTypes>
class callable_slot : public slot<ArgTypes...>
{
public:
callable_slot(Callable function_pointer_or_lambda) : callable(function_pointer_or_lambda) {}
virtual void operator()(const void*, ArgTypes... args) const override { callable(args...); }
private:
Callable callable;
};
template<typename Callable>
class callable_slot<Callable> : public slot<>
{
public:
callable_slot(Callable function_pointer_or_lambda) : callable(function_pointer_or_lambda) {}
virtual void operator()(const void*) const override { callable(); }
private:
Callable callable;
};
template<typename Callable, typename... ArgTypes>
using function_slot = callable_slot<Callable, ArgTypes...>;
I understand things like target
aren't implemented here, but I don't think any of the missing functions increase the size of the object.
What I'm asking is: why is std::function
larger in size than my cheap implementation above?
Your function_slot
takes a Callable
and set of args...
, and returns a type inheriting from slot<args...>
with a virtual operator()
.
To use it polymorphically as a value, you'd have to wrap it in a smart pointer and store it on the heap, and you'd have to forward the wrapping classes operator()
to the slot<args...>
one.
std::function
corresponds to that wrapper, not to your slot
or callable_slot
object.
template<class...Args>
struct smart_slot {
template<class Callable> // add SFINAE tests here TODO! IMPORTANT!
smart_slot( Callable other ):
my_slot( std::make_unique<callable_slot<Callable, Args...>>( std::move(other) ) )
{}
void operator()( Args...args ) const {
return (*my_slot)(std::forward<Args>(args)...);
}
// etc
private:
std::unique_ptr<slot<Args...>> my_slot;
};
smart_slot
is closer to std::function
than your code. As far as std::function
is concerned, everything you wrote is an implementation detail that users of std::function
wouldn't ever see.
Now, this would only require that std::function
be the size of one pointer. std::function
is larger because it has what is known as small object optimization.
Instead of just storing a smart pointer, it has a block of memory within itself. If the object you pass in fits in that block of memory, it constructs it in-place that block of memory instead of doing a heap allocation.
std::function
is basically mandated to do this for simple cases like being passed a function pointer. Quality implementations do it for larger and more complex objects. MSVC does it for objects up to the size of two std::string
s.
This means if you do this:
std::function<void(std::ostream&)> hello_world =
[s = "hello world"s](std::ostream& os)
{
os << s;
};
hello_world(std::cout);
it does no dynamic allocation on a decent implementation of std::function
.
Note that some major library vendors do dynamic allocation in this case.