Как реализована функция std:: function?

Согласно найденным источникам, выражение lambda по существу реализуется компилятором, создающим класс с перегруженным оператором вызова функции и ссылочными переменными в качестве членов. Это говорит о том, что размер лямбда-выражений меняется, и при наличии достаточных ссылочных переменных этот размер может быть сколь угодно большим.

An std::function должен иметь фиксированный размер, но он должен иметь возможность обертывать любые вызовы, включая любые лямбды того же типа. Как это реализовано? Если std::function внутренне использует указатель на свою цель, то что происходит, когда экземпляр std::function копируется или перемещается? Существуют ли какие-либо распределения кучи?

Ответ 1

Реализация std::function может отличаться от одной реализации к другой, но основная идея заключается в том, что она использует стирание типа. Хотя есть несколько способов сделать это, вы можете представить себе, что тривиальное (не оптимальное) решение может быть таким (упрощено для конкретного случая std::function<int (double)> для простоты):

struct callable_base {
   virtual int operator()(double d) = 0;
   virtual ~callable_base() {}
};
template <typename F>
struct callable : callable_base {
   F functor;
   callable(F functor) : functor(functor) {}
   virtual int operator()(double d) { return functor(d); }
};
class function_int_double {
   std::unique_ptr<callable_base> c;
public:
   template <typename F>
   function(F f) {
      c.reset(new callable<F>(f));
   }
   int operator()(double d) { return c(d); }
// ...
};

В этом простом подходе объект function будет хранить только unique_ptr для базового типа. Для каждого другого функтора, используемого с function, создается новый тип, основанный на базе, и объект этого типа создается динамически. Объект std::function всегда имеет одинаковый размер и выделяет пространство, необходимое для разных функторов в куче.

В реальной жизни существуют различные оптимизации, которые обеспечивают преимущества производительности, но усложнят ответ. Тип может использовать небольшие оптимизации объектов, динамическую отправку можно заменить указателем свободной функции, который принимает функтор в качестве аргумента, чтобы избежать одного уровня косвенности... но идея в основном такая же.


Что касается вопроса о том, как ведут себя копии std::function, быстрый тест указывает, что копии внутреннего вызываемого объекта выполняются, а не разделяют состояние.

// g++4.8
int main() {
   int value = 5;
   typedef std::function<void()> fun;
   fun f1 = [=]() mutable { std::cout << value++ << '\n' };
   fun f2 = f1;
   f1();                    // prints 5
   fun f3 = f1;
   f2();                    // prints 5
   f3();                    // prints 6 (copy after first increment)
}

Тест показывает, что f2 получает копию вызываемого объекта, а не ссылку. Если вызываемый объект был разделен различными объектами std::function<>, выход программы был бы равен 5, 6, 7.

Ответ 2

Для определенных типов аргументов ( "если f target является вызываемым объектом, переданным через reference_wrapper или указателем на функцию" ), конструктор std::function запрещает любые исключения, поэтому использование динамической памяти не может быть и речи. В этом случае все данные должны храниться непосредственно внутри объекта std::function.

В общем случае (включая лямбда-регистр) использование динамической памяти (через стандартный распределитель или распределитель, переданный конструктору std::function) допускается по мере соответствия реализации. В стандарте рекомендуется, чтобы реализация не использовала динамическую память, если ее можно избежать, но, как вы правильно говорите, если объект функции (а не объект std::function, но объект, обернутый внутри него), достаточно велик, нет никакого способа предотвратите его, так как std::function имеет фиксированный размер.

Это разрешение на выдачу исключений предоставляется как нормальному конструктору, так и конструктору копирования, который явно явно позволяет распределять динамическую память при копировании. Для ходов нет причин, по которым необходима динамическая память. Стандарт, похоже, явно не запрещает его, и, вероятно, не может, если этот ход может вызвать конструктор перемещения типа обернутого объекта, но вы должны быть уверены, что если реализация и ваши объекты разумны, перемещение не вызовет любые распределения.

Ответ 3

Ответ от @David Rodríguez - dribeas хорош для демонстрации стирания типа, но недостаточно хорош, так как стирание стилей также включает в себя копирование типов (в этом ответе объект функции не будет доступен для копирования). Эти поведения также хранятся в объекте function, кроме данных-функторов.

Трюк, используемый в реализации STL от Ubuntu 14.04 gcc 4.8, заключается в том, чтобы написать одну общую функцию, специализировать ее с каждым возможным типом функтора и направить их на универсальный тип указателя функции. Поэтому информация о типе стирается.

Я придумал упрощенную версию этого. Надеюсь, что это поможет

#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;
}

Есть также некоторые оптимизации в версии STL

  • construct_f и destroy_f смешиваются в один указатель на функцию (с дополнительным параметром, который сообщает, что делать), чтобы сохранить некоторые байты
  • raw-указатели используются для хранения объекта-функтора вместе с указателем функции в union, поэтому, когда объект function создается из указателя функции, он будет храниться непосредственно в union чем пустое пространство

Возможно, реализация STL не является лучшим решением, поскольку я слышал о некоторой более быстрой реализации. Однако я считаю, что основной механизм одинаков.

Ответ 4

An std::function перегружает operator(), делая объект-функтор, лямбда работает одинаково. Он в основном создает структуру с переменными-членами, к которым можно получить доступ внутри функции operator(). Поэтому основная концепция, которую следует иметь в виду, состоит в том, что лямбда - это объект (называемый функтором или функциональным объектом), а не функция. В стандарте говорится, что нельзя использовать динамическую память, если ее можно избежать.