Zebrafish
Zebrafish

Reputation: 13876

Does std::function have virtual functions for copying and moving?

If we store std::functionss in a vector, and it needs to resize and reallocate the copy constructors or the move constructors for std::function will be called. Generally speaking lambdas and functors hold trivially moveable and destructible objects, but in the case that it captures something non trivially destructible or trivially movable or copyable then it needs to have a vtable on how to do these operations, right?

Here is an example of std::function being able to take any callable object, the std::function must have function pointers or virtual functions to move, copy and destroy the object correctly, right?

    struct CustomFunctor
    {
        
        std::vector<int> buffer;
        CustomFunctor() {}
        
        

int operator()() { return 7; }
};

int main()
{
    
    std::function<void(void)> f{ CustomFunctor() }; /* NEEDS TO CALL CUSTOM DESTRUCTOR AND COPY AND MOVE FUNCTIONS */

    f = []() { return 7; };
    /* IF IT CALLS DESTRUCTOR OR MOVE AND COPY FUNCTIONS BASED ON TEMPLATE
                                                  THERE'S TROUBLE, RIGHT?*/
}

Upvotes: 3

Views: 497

Answers (1)

Jonathan S.
Jonathan S.

Reputation: 1854

std::function does not, technically speaking, have virtual member functions. GCC's implementation can be found here in case you want to see all the nitty-gritty details for yourself. It certainly doesn't have a vtable; instead, it uses a pair of function pointers - one for invoking the function, and one for managing the object (destruction, copying, moving). These function pointers are specific for the type of callable that's currently stored in the function object; therefore, the functions they point to know what the underlying callable type is.

These two function pointers get populated by the constructor of std::function, which has access to the exact type of callable being assigned to the function object. Therefore it can instantiate appropriate function templates (invoker and manager functions) and place pointers to these functions in the function object.

In summary, std::function is typically implemented something like this. (This is most likely not fully accurate, but it should get the general idea across.)

enum class ManagerOp {
  DESTROY,
  COPY_ASSIGN_TO,
  MOVE_ASSIGN_TO
};

template<typename Callable, typename Ret, typename... Args>
struct function_callable_implementation {
  static Ret invoker_function(not_std_function *self, Args...) { /* call it */ }
  static void manager_function(not_std_function *self, not_std_function *other, ManagerOp op) {
    switch (op) {
      case DESTROY: /* destroy the callable */ break;
      case COPY_ASSIGN_TO: /* copy the callable */ break;
      case MOVE_ASSIGN_TO: /* move the callable */ break;
      default: /* unreachable */
    }
  }
}


template<typename Ret, typename... Args>
struct not_std_function {
  using InvokerType = Ret (*)(not_std_function *self, Args...);
  using ManagerType = void (*) (not_std_function *self, not_std_function *other, ManagerOp op);
  
  InvokerType m_invoker = nullptr;
  ManagerType m_manager = nullptr;
  
  union {
    void *m_callableInstance;
    char m_smallTypeOptimizationBuffer[IMPLEMENTATION_DEFINED_BUT_PROBABLY_16_BYTES];
  }
  
  ~not_std_function() {
    if (m_manager) m_manager(this, nullptr, ManagerOp::DESTROY);
  }
  
  not_std_function& operator= (const not_std_function& other) {
    if (this == &other) return *this;
    
    if (m_manager) m_manager(this, nullptr, ManagerOp::DESTROY);
    if (other.m_manager) other.m_manager((not_std_function *)&other, this, ManagerOp::COPY_ASSIGN_TO);
    
    return *this;
  }
  
  /* same thing for move assign, copy construct, move construct */
  
  not_std_function(auto callable) {
    using Stuff = function_callable_implementation<decltype(callable), Ret, Args...>;
    
    m_invoker = &Stuff::invoker_function;
    m_manager = &Stuff::manager_function;
    
    /* initialize m_callableInstance or m_smallTypeOptimizationBuffer */
  }
};

This kind of implementation ensures that there will be no issues with slicing; it's a nice value type that can be copied, stored in vectors, etc without having to worry about virtual functions.

As another example, here's a std::any-like class that can be used as the key to a map. It uses the same "trick" of holding a set of function pointers to achieve type erasure without compromising type-safety. Compared to std::function, my ttlhacker::any_key implementation uses one additional level of indirection, though.

Upvotes: 3

Related Questions