La respuesta de @David Rodríguez: dribeas es buena para demostrar el borrado de tipo, pero no lo suficientemente bueno, ya que el borrado de tipo también incluye cómo se copian los tipos (en esa respuesta, el objeto de función no se podrá copiar). Estos comportamientos también se almacenan en el function
objeto, además de los datos de functor.
El truco, utilizado en la implementación de STL de Ubuntu 14.04 gcc 4.8, es escribir una función genérica, especializarla con cada tipo de functor posible y convertirlos en un tipo de puntero de función universal. Por lo tanto, se borra la información de tipo .
He improvisado una versión simplificada de eso. Espero que ayude
#include <iostream>
#include <memory>
template <typename T>
class function;
template <typename R, typename... Args>
class function<R(Args...)>
{
// function pointer types for the type-erasure behaviors
// all these char* parameters are actually casted from some functor type
typedef R (*invoke_fn_t)(char*, Args&&...);
typedef void (*construct_fn_t)(char*, char*);
typedef void (*destroy_fn_t)(char*);
// type-aware generic functions for invoking
// the specialization of these functions won't be capable with
// the above function pointer types, so we need some cast
template <typename Functor>
static R invoke_fn(Functor* fn, Args&&... args)
{
return (*fn)(std::forward<Args>(args)...);
}
template <typename Functor>
static void construct_fn(Functor* construct_dst, Functor* construct_src)
{
// the functor type must be copy-constructible
new (construct_dst) Functor(*construct_src);
}
template <typename Functor>
static void destroy_fn(Functor* f)
{
f->~Functor();
}
// these pointers are storing behaviors
invoke_fn_t invoke_f;
construct_fn_t construct_f;
destroy_fn_t destroy_f;
// erase the type of any functor and store it into a char*
// so the storage size should be obtained as well
std::unique_ptr<char[]> data_ptr;
size_t data_size;
public:
function()
: invoke_f(nullptr)
, construct_f(nullptr)
, destroy_f(nullptr)
, data_ptr(nullptr)
, data_size(0)
{}
// construct from any functor type
template <typename Functor>
function(Functor f)
// specialize functions and erase their type info by casting
: invoke_f(reinterpret_cast<invoke_fn_t>(invoke_fn<Functor>))
, construct_f(reinterpret_cast<construct_fn_t>(construct_fn<Functor>))
, destroy_f(reinterpret_cast<destroy_fn_t>(destroy_fn<Functor>))
, data_ptr(new char[sizeof(Functor)])
, data_size(sizeof(Functor))
{
// copy the functor to internal storage
this->construct_f(this->data_ptr.get(), reinterpret_cast<char*>(&f));
}
// copy constructor
function(function const& rhs)
: invoke_f(rhs.invoke_f)
, construct_f(rhs.construct_f)
, destroy_f(rhs.destroy_f)
, data_size(rhs.data_size)
{
if (this->invoke_f) {
// when the source is not a null function, copy its internal functor
this->data_ptr.reset(new char[this->data_size]);
this->construct_f(this->data_ptr.get(), rhs.data_ptr.get());
}
}
~function()
{
if (data_ptr != nullptr) {
this->destroy_f(this->data_ptr.get());
}
}
// other constructors, from nullptr, from function pointers
R operator()(Args&&... args)
{
return this->invoke_f(this->data_ptr.get(), std::forward<Args>(args)...);
}
};
// examples
int main()
{
int i = 0;
auto fn = [i](std::string const& s) mutable
{
std::cout << ++i << ". " << s << std::endl;
};
fn("first"); // 1. first
fn("second"); // 2. second
// construct from lambda
::function<void(std::string const&)> f(fn);
f("third"); // 3. third
// copy from another function
::function<void(std::string const&)> g(f);
f("forth - f"); // 4. forth - f
g("forth - g"); // 4. forth - g
// capture and copy non-trivial types like std::string
std::string x("xxxx");
::function<void()> h([x]() { std::cout << x << std::endl; });
h();
::function<void()> k(h);
k();
return 0;
}
También hay algunas optimizaciones en la versión STL.
- el
construct_f
y destroy_f
se mezclan en un puntero de función (con un parámetro adicional que le dice qué hacer) como para guardar algunos bytes
- Los punteros en bruto se utilizan para almacenar el objeto de función, junto con un puntero de función en a
union
, de modo que cuando un function
objeto se construye a partir de un puntero de función, se almacenará directamente en el union
espacio en lugar de en el montón
Quizás la implementación de STL no sea la mejor solución, ya que he oído hablar de una implementación más rápida . Sin embargo, creo que el mecanismo subyacente es el mismo.
std::function
un tiempo. Es esencialmente una clase de identificador para un objeto polimórfico. Se crea una clase derivada de la clase base interna para contener los parámetros, asignados en el montón; luego, el puntero a esto se mantiene como un subobjeto destd::function
. Creo que utiliza el recuento de referencias comostd::shared_ptr
para manejar la copia y el movimiento.