Bagaimana std :: function diimplementasikan?

99

Menurut sumber yang saya temukan, ekspresi lambda pada dasarnya diimplementasikan oleh kompilator yang membuat kelas dengan operator panggilan fungsi yang kelebihan beban dan variabel yang direferensikan sebagai anggota. Hal ini menunjukkan bahwa ukuran ekspresi lambda bervariasi, dan diberikan variabel referensi yang cukup sehingga ukurannya dapat menjadi besar secara sewenang-wenang .

An std::functionharus memiliki ukuran tetap , tetapi harus dapat membungkus segala jenis callable, termasuk lambda apa pun dari jenis yang sama. Bagaimana penerapannya? Jika secara std::functioninternal menggunakan pointer ke targetnya, lalu apa yang terjadi, ketika std::functioninstance disalin atau dipindahkan? Apakah ada alokasi heap yang terlibat?

Miklós Homolya
sumber
2
Saya melihat implementasi gcc / stdlib std::functionbeberapa waktu lalu. Ini pada dasarnya adalah kelas pegangan untuk objek polimorfik. Kelas turunan dari kelas dasar internal dibuat untuk menampung parameter, dialokasikan pada heap - kemudian penunjuk ke ini dipegang sebagai subobjek dari std::function. Saya percaya itu menggunakan penghitungan referensi seperti std::shared_ptruntuk menangani penyalinan dan pemindahan.
Andrew Tomazos
4
Perhatikan bahwa implementasi mungkin menggunakan sihir, yaitu bergantung pada ekstensi kompilator yang tidak tersedia untuk Anda. Ini sebenarnya diperlukan untuk beberapa ciri tipe. Secara khusus, trampolin adalah teknik yang diketahui tidak tersedia dalam C ++ standar.
MSalters

Jawaban:

80

Implementasi std::functiondapat berbeda dari satu implementasi ke implementasi lainnya, tetapi ide intinya adalah menggunakan tipe-erasure. Meskipun ada banyak cara untuk melakukannya, Anda dapat membayangkan solusi yang sepele (tidak optimal) bisa seperti ini (disederhanakan untuk kasus khusus std::function<int (double)>demi kesederhanaan):

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

Dalam pendekatan sederhana ini, functionobjek hanya akan menyimpan unique_ptrke tipe dasar. Untuk setiap functor berbeda yang digunakan dengan function, tipe baru yang diturunkan dari basis dibuat dan objek dari tipe itu dibuat secara dinamis. The std::functionobjek selalu dengan ukuran yang sama dan akan mengalokasikan ruang yang diperlukan untuk functors yang berbeda dalam tumpukan.

Dalam kehidupan nyata, ada berbagai pengoptimalan yang memberikan keunggulan kinerja tetapi akan mempersulit jawabannya. Jenisnya dapat menggunakan pengoptimalan objek kecil, pengiriman dinamis dapat diganti dengan penunjuk fungsi bebas yang menggunakan functor sebagai argumen untuk menghindari satu tingkat tipuan ... tetapi idenya pada dasarnya sama.


Mengenai masalah bagaimana salinan std::functionberperilaku, tes cepat menunjukkan bahwa salinan objek internal yang dapat dipanggil sudah selesai, daripada membagikan status.

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

Pengujian tersebut menunjukkan bahwa f2mendapatkan salinan dari entitas yang dapat dipanggil, bukan referensi. Jika entitas yang dapat dipanggil dibagikan oleh std::function<>objek yang berbeda , output dari program akan menjadi 5, 6, 7.

David Rodríguez - dribeas
sumber
@Cole "Cole9" Johnson menebak dia menulisnya sendiri
aaronman
8
@Cole "Cole9" Johnson: Ini adalah penyederhanaan yang berlebihan dari kode asli, saya baru saja mengetiknya di browser, jadi mungkin ada kesalahan ketik dan / atau gagal untuk dikompilasi karena alasan yang berbeda. Kode dalam jawaban hanya ada untuk menyajikan bagaimana penghapusan jenis / dapat diterapkan, ini jelas bukan kode kualitas produksi.
David Rodríguez - dribeas
2
@MooingDuck: Saya percaya lambda dapat disalin (5.1.2 / 19), tetapi itu bukan pertanyaannya, melainkan apakah semantik std::functionakan benar jika objek internal disalin, dan saya rasa tidak demikian (pikirkan lambda yang menangkap nilai dan bisa berubah, disimpan di dalam std::function, jika status fungsi disalin jumlah salinan std::functiondi dalam algoritme standar dapat menghasilkan hasil yang berbeda, yang tidak diinginkan.
David Rodríguez - dribeas
1
@ MiklósHomolya: Saya menguji dengan g ++ 4.8 dan implementasinya menyalin status internal. Jika entitas yang dapat dipanggil cukup besar untuk memerlukan alokasi dinamis, maka salinan dari std::functionakan memicu alokasi.
David Rodríguez - dribeas
4
Status bersama @ DavidRodríguez-dribeas tidak akan diinginkan, karena pengoptimalan objek kecil akan berarti bahwa Anda akan beralih dari status bersama ke status tidak dibagi pada batas ukuran yang ditentukan versi compiler dan versi compiler (karena pengoptimalan objek kecil akan memblokir status bersama). Sepertinya itu bermasalah.
Yakk - Adam Nevraumont
23

Jawaban dari @David Rodríguez - dribeas bagus untuk mendemonstrasikan penghapusan jenis tetapi tidak cukup baik karena jenis-penghapusan juga mencakup bagaimana jenis disalin (dalam jawaban itu objek fungsi tidak akan dapat dibuat-salinan). Perilaku tersebut juga disimpan di functionobjek, selain data functor.

Triknya, yang digunakan dalam implementasi STL dari Ubuntu 14.04 gcc 4.8, adalah menulis satu fungsi umum, mengkhususkan dengan setiap jenis fungsi yang memungkinkan, dan mentransmisikannya ke jenis penunjuk fungsi universal. Oleh karena itu informasi tipe dihapus .

Saya telah membuat versi sederhana dari itu. Semoga bisa membantu

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

Ada juga beberapa pengoptimalan dalam versi STL

  • yang construct_fdan destroy_fdicampur menjadi satu fungsi pointer (dengan parameter tambahan yang memberitahu apa yang harus dilakukan) untuk menyimpan beberapa byte
  • pointer mentah digunakan untuk menyimpan objek functor, bersama dengan pointer fungsi di a union, sehingga ketika functionobjek dibangun dari pointer fungsi, itu akan disimpan secara langsung di ruang unionbukan heap

Mungkin implementasi STL bukanlah solusi terbaik karena saya pernah mendengar tentang implementasi yang lebih cepat . Namun saya yakin mekanisme dasarnya sama.

neuront
sumber
20

Untuk jenis argumen tertentu ("jika target f adalah objek yang dapat dipanggil yang diteruskan melalui reference_wrapperatau penunjuk fungsi"), std::functionkonstruktor tidak mengizinkan pengecualian apa pun, jadi menggunakan memori dinamis bukanlah pertanyaan. Untuk kasus ini, semua data harus disimpan langsung di dalam std::functionobjek.

Dalam kasus umum, (termasuk kasus lambda), menggunakan memori dinamis (baik melalui pengalokasi standar, atau pengalokasi yang diteruskan ke std::functionkonstruktor) diperbolehkan jika implementasi dirasa cocok. Standar merekomendasikan implementasi tidak menggunakan memori dinamis jika itu dapat dihindari, tetapi seperti yang Anda katakan dengan benar, jika objek fungsi (bukan std::functionobjek, tetapi objek yang dibungkus di dalamnya) cukup besar, tidak ada cara untuk mencegahnya, karena std::functionmemiliki ukuran tetap.

Izin untuk membuang pengecualian ini diberikan ke konstruktor normal dan konstruktor salinan, yang secara cukup eksplisit memungkinkan alokasi memori dinamis selama penyalinan juga. Untuk bergerak, tidak ada alasan mengapa memori dinamis diperlukan. Standar tampaknya tidak secara eksplisit melarangnya, dan mungkin tidak bisa jika pemindahan dapat memanggil konstruktor pemindahan dari tipe objek yang dibungkus, tetapi Anda harus dapat berasumsi bahwa jika implementasi dan objek Anda masuk akal, pemindahan tidak akan menyebabkan alokasi apa pun.


sumber
-6

An std::functionmembebani operator()menjadikannya sebagai objek functor, lambda bekerja dengan cara yang sama. Ini pada dasarnya membuat struct dengan variabel anggota yang dapat diakses di dalam operator()fungsi. Jadi konsep dasar yang perlu diingat adalah bahwa lambda adalah objek (disebut fungsi atau fungsi) bukan fungsi. Standar tersebut mengatakan untuk tidak menggunakan memori dinamis jika dapat dihindari.

aaronman
sumber
1
Bagaimana mungkin lambda besar yang sewenang-wenang bisa masuk ke dalam ukuran tetap std::function? Itulah pertanyaan kuncinya di sini.
Miklós Homolya
2
@aaronman: Saya menjamin bahwa setiap std::functionobjek memiliki ukuran yang sama, dan bukan ukuran lambda yang terkandung.
Mooing Duck
5
@aaronman dengan cara yang sama bahwa setiap std::vector<T...> objek memiliki ukuran tetap (waktu-waktu) tetap tidak bergantung pada instance / jumlah elemen pengalokasi aktual.
lihat
3
@aaronman: Yah, mungkin Anda harus menemukan pertanyaan stackoverflow yang menjawab bagaimana std :: function diimplementasikan sedemikian rupa sehingga dapat berisi
lambda
1
@aaronman: Ketika entitas yang dapat dipanggil disetel, dalam konstruksi, penugasan ... std::function<void ()> f;tidak perlu mengalokasikan di sana, std::function<void ()> f = [&]() { /* captures tons of variables */ };kemungkinan besar mengalokasikan. std::function<void()> f = &free_function;mungkin juga tidak mengalokasikan ...
David Rodríguez - dribeas