Як реалізована функція std ::?


98

Згідно з джерелами, які я знайшов, лямбда-вираз по суті реалізується компілятором, створюючи клас із перевантаженим оператором виклику функції та посиланими змінними як членами. Це свідчить про те, що розмір лямбда-виразів варіюється, і з урахуванням достатньої кількості посилальних змінних, розмір яких може бути довільно великим .

Антон std::functionповинен мати фіксований розмір , але він повинен мати можливість обертати будь-який тип викликів, включаючи будь-які лямбди того ж виду. Як це реалізується? Якщо std::functionвнутрішньо використовує вказівник на свою ціль, то що відбувається, коли std::functionекземпляр копіюється або переміщується? Чи є якісь розподіли купи?


2
Я розглядав реалізацію gcc / stdlib std::functionдеякий час тому. По суті, це клас дескриптора для поліморфного об’єкта. Похідний клас внутрішнього базового класу створюється для зберігання параметрів, виділених у купі, - тоді вказівник на нього зберігається як суб'єкт std::function. Я вважаю, що він використовує підрахунок посилань, як std::shared_ptrдля обробки копіювання та переміщення.
Ендрю Томазос,

4
Зверніть увагу, що реалізації можуть використовувати магію, тобто покладатися на розширення компілятора, які вам недоступні. Це насправді необхідно для деяких типів рис. Зокрема, батути - це відома техніка, недоступна у стандартній C ++.
MSalters

Відповіді:


78

Реалізація 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.


@Cole "Cole9" Джонсон, здогадуючись, що він написав це сам
aaronman

8
@Cole "Cole9" Джонсон: Це надто спрощення реального коду, я просто набрав його у браузері, тому він може мати помилки друку та / або не компілювати з різних причин. Код у відповіді просто там, щоб представити, як стирання типу може / може бути реалізоване, це явно не код якості виробництва.
Девід Родрігес - dribeas

2
@MooingDuck: Я справді вважаю, що лямбди можна копіювати (5.1.2 / 19), але це не питання, швидше, чи була std::functionби семантика правильна, якби внутрішній об'єкт був скопійований, і я не думаю, що це має бути так (думаю, лямбда, яка фіксує значення і є змінною, зберігається всередині std::function, якщо стан функції скопійовано, кількість копій std::functionусередині стандартного алгоритму може призвести до різних результатів, що є небажаним.
Девід Родрігес - dribeas

1
@ MiklósHomolya: Я тестував з g ++ 4.8, і реалізація копіює внутрішній стан. Якщо виклична сутність є достатньо великою, щоб вимагати динамічного розподілу, тоді копія std::functionзапустить розподіл.
Девід Родрігес - dribeas

4
Спільний стан @ DavidRodríguez-dribeas було б небажаним, оскільки оптимізація малого об'єкта означала б, що ви переходите із загального стану до несподіленого стану в порозі розміру компілятора та компілятора (оскільки оптимізація малого об'єкта блокує загальний стан). Це здається проблематичним.
Якк - Адам Неврамон

22

Відповідь від @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 змішуються в один покажчик на функцію (з додатковим параметром , який говорить , що робити), щоб зберегти кілька байт
  • необроблені вказівники використовуються для зберігання об'єкта функтора, разом з покажчиком функції в a union, так що коли functionоб'єкт будується з покажчика функції, він буде зберігатися безпосередньо в unionпросторі, а не в купі

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


20

Для певних типів аргументів ("якщо ціль f - викличний об'єкт, переданий через reference_wrapperабо покажчик функції"), std::functionконструктор забороняє будь-які винятки, тому про використання динамічної пам'яті мова не може йти. У цьому випадку всі дані повинні зберігатися безпосередньо всерединіstd::function об’єкта.

У загальному випадку, (включаючи лямбда-випадок), використання динамічної пам'яті (або за допомогою стандартного розподільника, або розподілювача, переданого std::functionконструктору) дозволяється відповідно до реалізації. Стандарт рекомендує реалізації не використовувати динамічну пам’ять, якщо цього можна уникнути, але, як ви справедливо говорите, якщо об’єкт функції (не std::functionоб’єкт, а об’єкт, що обертається всередині нього) є достатньо великим, немає можливості запобігти цьому, оскількиstd::function має фіксований розмір.

Цей дозвіл на викидання винятків надається як звичайному конструктору, так і конструктору копіювання, який досить явно дозволяє динамічне виділення пам'яті під час копіювання. Для ходів немає жодної причини, через яку потрібна динамічна пам’ять. Стандарт, здається, явно не забороняє, і, можливо, не може, якщо переміщення може викликати конструктор переміщення типу обгорнутого об'єкта, але ви повинні мати змогу припустити, що якщо і реалізація, і ваші об'єкти розумні, переміщення не спричинить будь-які розподіли.


-6

An std::functionперевантажує operator()робить його функтор об'єкт, робота Lambda по таким же чином. В основному він створює структуру зі змінними-членами, до яких можна отримати доступ всередині operator()функції. Отже, основне поняття, про яке слід пам’ятати, полягає в тому, що лямбда - це об’єкт (який називається функтором або об’єктом функції), а не функцією. Стандарт говорить, що не слід використовувати динамічну пам’ять, якщо цього можна уникнути.


1
Як, можливо, довільно великі лямбди вкладаються у фіксований розмір std::function? Це ключове питання тут.
Miklós Homolya

2
@aaronman: Я гарантую, що кожен std::functionоб’єкт однакового розміру і не є розміром лямбди, що містяться в ньому.
Mooing Duck

5
@aaronman так само, як кожен std::vector<T...> об'єкт має фіксований розмір (час копіювання), незалежно від фактичного екземпляра розподільника / кількості елементів.
sehe

3
@aaronman: Ну, можливо, вам слід знайти запитання stackoverflow, яке відповідає, як реалізована функція std :: таким чином, що вона може містити лямбди довільного розміру: P
Mooing Duck

1
@aaronman: Коли встановлена виклична сутність, при побудові, присвоєнні ... std::function<void ()> f;немає необхідності виділяти там, std::function<void ()> f = [&]() { /* captures tons of variables */ };швидше за все, виділяє. std::function<void()> f = &free_function;ймовірно, не виділяє ні ...
Девід Родрігес - дрібас
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.