Search code examples
c++c++11functional-programmingstd-function

What does my slot class miss that std::function has?


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?


Solution

  • 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::strings.

    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.