Come viene implementata la funzione std ::?


98

Secondo le fonti che ho trovato, un'espressione lambda viene essenzialmente implementata dal compilatore creando una classe con operatore di chiamata di funzione sovraccarico e le variabili a cui si fa riferimento come membri. Ciò suggerisce che la dimensione delle espressioni lambda varia e, dato un numero sufficiente di variabili di riferimento, la dimensione può essere arbitrariamente grande .

An std::functiondovrebbe avere una dimensione fissa , ma deve essere in grado di avvolgere qualsiasi tipo di callable, inclusi eventuali lambda dello stesso tipo. Come viene implementato? Se std::functioninternamente utilizza un puntatore alla sua destinazione, cosa succede quando l' std::functionistanza viene copiata o spostata? Sono coinvolte allocazioni di heap?


2
Ho esaminato l'implementazione di gcc / stdlib std::functionqualche tempo fa. È essenzialmente una classe handle per un oggetto polimorfico. Viene creata una classe derivata della classe base interna per contenere i parametri, allocati sull'heap, quindi il puntatore a questo viene mantenuto come un oggetto secondario di std::function. Credo che utilizzi il conteggio dei riferimenti std::shared_ptrper gestire la copia e lo spostamento.
Andrew Tomazos

4
Nota che le implementazioni possono usare la magia, cioè fare affidamento su estensioni del compilatore che non sono disponibili. Questo è infatti necessario per alcuni tratti di tipo. In particolare, i trampolini sono una tecnica nota non disponibile nel C ++ standard.
MSalters

Risposte:


78

L'implementazione di std::functionpuò differire da un'implementazione all'altra, ma l'idea centrale è che utilizza la cancellazione del tipo. Sebbene ci siano diversi modi per farlo, puoi immaginare che una soluzione banale (non ottimale) potrebbe essere come questa (semplificata per il caso specifico di std::function<int (double)>per semplicità):

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); }
// ...
};

In questo semplice approccio, l' functionoggetto memorizzerebbe solo a unique_ptrin un tipo di base. Per ogni diverso funtore utilizzato con function, viene creato un nuovo tipo derivato dalla base e un oggetto di quel tipo viene istanziato dinamicamente. L' std::functionoggetto è sempre della stessa dimensione e allocherà lo spazio necessario per i diversi funtori nell'heap.

Nella vita reale ci sono diverse ottimizzazioni che forniscono vantaggi in termini di prestazioni ma complicherebbero la risposta. Il tipo potrebbe utilizzare piccole ottimizzazioni di oggetti, l'invio dinamico può essere sostituito da un puntatore a funzione libera che prende il funtore come argomento per evitare un livello di riferimento indiretto ... ma l'idea è fondamentalmente la stessa.


Per quanto riguarda il problema di come si std::functioncomportano le copie del file , un rapido test indica che vengono eseguite le copie dell'oggetto richiamabile interno, anziché condividere lo stato.

// 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)
}

Il test indica che f2ottiene una copia dell'entità richiamabile, anziché un riferimento. Se l'entità richiamabile fosse condivisa dai diversi std::function<>oggetti, l'output del programma sarebbe stato 5, 6, 7.


@Cole "Cole9" Johnson immagina di averlo scritto lui stesso
Aaronman

8
@Cole "Cole9" Johnson: Questa è una semplificazione eccessiva del codice reale, l'ho appena digitato nel browser, quindi potrebbe avere errori di battitura e / o non riuscire a compilare per diversi motivi. Il codice nella risposta serve solo a presentare come è / può essere implementata la cancellazione del tipo, questo chiaramente non è un codice di qualità della produzione.
David Rodríguez - dribeas

2
@MooingDuck: credo che i lambda siano copiabili (5.1.2 / 19), ma non è questo il problema, piuttosto se la semantica di std::functionsarebbe corretta se l'oggetto interno fosse stato copiato, e non penso che sia così (si pensi a un lambda che cattura un valore ed è modificabile, memorizzato in un std::function, se lo stato della funzione fosse copiato il numero di copie di std::functionall'interno di un algoritmo standard potrebbe portare a risultati diversi, il che è indesiderato.
David Rodríguez - dribeas

1
@ MiklósHomolya: ho testato con g ++ 4.8 e l'implementazione copia lo stato interno. Se l'entità richiamabile è abbastanza grande da richiedere un'allocazione dinamica, la copia di std::functionattiverà un'allocazione.
David Rodríguez - dribeas

4
@ DavidRodríguez-dribeas shared state sarebbe indesiderabile, perché l'ottimizzazione di oggetti piccoli significherebbe che si passerebbe da stato condiviso a stato non condiviso a una soglia di dimensione determinata dalla versione del compilatore e del compilatore (poiché l'ottimizzazione di oggetti piccoli bloccherebbe lo stato condiviso). Sembra problematico.
Yakk - Adam Nevraumont

22

La risposta di @David Rodríguez - dribeas è utile per dimostrare la cancellazione del tipo ma non abbastanza buona poiché la cancellazione del tipo include anche il modo in cui i tipi vengono copiati (in quella risposta l'oggetto funzione non sarà costruibile dalla copia). Questi comportamenti sono anche memorizzati functionnell'oggetto, oltre ai dati del funtore.

Il trucco, utilizzato nell'implementazione STL da Ubuntu 14.04 gcc 4.8, è scrivere una funzione generica, specializzarla con ogni possibile tipo di funtore e lanciarli in un tipo di puntatore a funzione universale. Pertanto le informazioni sul tipo vengono cancellate .

Ho messo a punto una versione semplificata di questo. Spero che ti aiuti

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

Ci sono anche alcune ottimizzazioni nella versione STL

  • la construct_fe destroy_fsi mescolano in un puntatore a funzione (con un parametro aggiuntivo che dice cosa fare) per salvare alcuni byte
  • i puntatori non elaborati vengono utilizzati per memorizzare l'oggetto funtore, insieme a un puntatore a funzione in a union, in modo che quando un functionoggetto viene costruito da un puntatore a funzione, verrà memorizzato direttamente nello unionspazio anziché nell'heap

Forse l'implementazione STL non è la soluzione migliore poiché ho sentito parlare di un'implementazione più rapida . Tuttavia credo che il meccanismo sottostante sia lo stesso.


20

Per alcuni tipi di argomenti ("se l'obiettivo di f è un oggetto richiamabile passato tramite reference_wrappero un puntatore a funzione"), std::functionil costruttore di s non consente eccezioni, quindi l'uso della memoria dinamica è fuori questione. In questo caso, tutti i dati devono essere memorizzati direttamente all'interno std::functiondell'oggetto.

Nel caso generale, (incluso il caso lambda), l'utilizzo della memoria dinamica (tramite l'allocatore standard o un allocatore passato al std::functioncostruttore) è consentito secondo l'implementazione. Lo standard raccomanda alle implementazioni di non utilizzare la memoria dinamica se può essere evitata, ma come dici giustamente, se l'oggetto funzione (non l' std::functionoggetto, ma l'oggetto che viene avvolto al suo interno) è abbastanza grande, non c'è modo di impedirlo, poiché std::functionha una dimensione fissa.

Questa autorizzazione a generare eccezioni è concessa sia al costruttore normale che al costruttore di copia, che consente in modo abbastanza esplicito anche allocazioni di memoria dinamica durante la copia. Per le mosse, non c'è motivo per cui sarebbe necessaria la memoria dinamica. Lo standard non sembra proibirlo esplicitamente, e probabilmente non lo può fare se la mossa potrebbe chiamare il costruttore di mosse del tipo di oggetto avvolto, ma dovresti essere in grado di presumere che se sia l'implementazione che i tuoi oggetti sono ragionevoli, lo spostamento non causerà eventuali assegnazioni.


-6

Un std::functionsovraccarica il operator()rendendolo un oggetto funtore, il lavoro di lambda allo stesso modo. Fondamentalmente crea una struttura con variabili membro a cui è possibile accedere all'interno della operator()funzione. Quindi il concetto di base da tenere a mente è che un lambda è un oggetto (chiamato funtore o oggetto funzione) non una funzione. Lo standard dice di non utilizzare la memoria dinamica se può essere evitata.


1
Come possono lambda arbitrariamente grandi rientrare in una dimensione fissa std::function? Questa è la domanda chiave qui.
Miklós Homolya

2
@aaronman: garantisco che ogni std::functionoggetto ha le stesse dimensioni e non sono le dimensioni dei lambda contenuti.
Mooing Duck

5
@aaronman nello stesso modo in cui ogni std::vector<T...> oggetto ha una dimensione fissa (copiletime) indipendente dall'istanza / numero di elementi dell'allocatore effettivo.
guarda il

3
@aaronman: Beh, forse dovresti trovare una domanda di stackoverflow che risponda a come std :: function è implementata in modo tale da poter contenere lambda di dimensioni arbitrarie: P
Mooing Duck

1
@aaronman: Quando l' entità richiamabile è impostata, in costruzione, assegnazione ... std::function<void ()> f;non c'è bisogno di allocare lì, std::function<void ()> f = [&]() { /* captures tons of variables */ };molto probabilmente alloca. std::function<void()> f = &free_function;probabilmente non assegna neanche ...
David Rodríguez - dribeas
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.