Std :: function nasıl uygulanır?


100

Bulduğum kaynaklara göre, bir lambda ifadesi esasen derleyici tarafından aşırı yüklenmiş işlev çağrısı operatörü ve üye olarak başvurulan değişkenler içeren bir sınıf yaratarak uygulanıyor. Bu, lambda ifadelerinin boyutunun değiştiğini ve boyutun keyfi olarak büyük olabileceği yeterli referans değişkenleri verildiğini gösterir .

An sabit bir boyutastd::function sahip olmalıdır , ancak aynı türden lambdalar da dahil olmak üzere her türlü çağrılabilir nesneyi sarmalayabilmelidir. Nasıl uygulanıyor? Eğer içten hedefine bir işaretçi kullanır, o zaman ne olur, ne zaman örneği kopyalanmış veya taşınır? Herhangi bir yığın tahsisi var mı?std::functionstd::function


2
std::functionBir süre önce gcc / stdlib uygulamasına baktım . Esasen bir polimorfik nesne için bir tutamaç sınıfıdır. Yığın üzerinde ayrılan parametreleri tutmak için dahili temel sınıfın türetilmiş bir sınıfı yaratılır - daha sonra buna işaretçi, öğesinin bir alt nesnesi olarak tutulur std::function. std::shared_ptrKopyalama ve taşıma gibi referans sayma yöntemini kullandığına inanıyorum .
Andrew Tomazos

4
Gerçekleştirmelerin sihir kullanabileceğini unutmayın, yani sizin için mevcut olmayan derleyici uzantılarına güvenebilirsiniz. Bu aslında bazı tip özellikleri için gereklidir. Özellikle, trambolinler , standart C ++ 'da bulunmayan bilinen bir tekniktir.
MSalters

Yanıtlar:


80

Uygulaması, std::functionbir uygulamadan diğerine farklılık gösterebilir, ancak temel fikir, tür silmeyi kullanmasıdır. Bunu yapmanın birden fazla yolu olsa da, önemsiz (optimal olmayan) bir çözümün şöyle olabileceğini hayal edebilirsiniz std::function<int (double)>(basitlik uğruna belirli bir durum için basitleştirilmiştir):

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

Bu basit yaklaşımda, functionnesne sadece unique_ptrbir taban tipini depolayacaktır . İle kullanılan her farklı işlev için function, temelden türetilen yeni bir tür oluşturulur ve bu türden bir nesne dinamik olarak başlatılır. std::functionNesne her zaman aynı boyutta ve yığın içinde farklı functors için gereken alanı tahsis edecektir.

Gerçek hayatta, performans avantajları sağlayan ancak yanıtı karmaşıklaştıracak farklı optimizasyonlar vardır. Tür, küçük nesne optimizasyonlarını kullanabilir, dinamik gönderim, bir düzey dolaylı yoldan kaçınmak için functor'u argüman olarak alan serbest işlevli bir işaretçi ile değiştirilebilir ... ancak fikir temelde aynıdır.


Kopyalarının nasıl std::functiondavrandığı konusuyla ilgili olarak, hızlı bir test, durumu paylaşmak yerine dahili çağrılabilir nesnenin kopyalarının yapıldığını gösterir.

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

Test, f2bir referans yerine çağrılabilir varlığın bir kopyasını aldığını gösterir . Çağrılabilir varlık farklı std::function<>nesneler tarafından paylaşılmış olsaydı, programın çıktısı 5, 6, 7 olurdu.


@Cole "Cole9" Johnson bunu kendisinin yazdığını tahmin ediyor
aaronman

8
@Cole "Cole9" Johnson: Bu, gerçek kodun aşırı basitleştirilmesidir, tarayıcıya yazdım, bu nedenle yazım hataları olabilir ve / veya farklı nedenlerle derleme başarısız olabilir. Cevaptaki kod, sadece tür silme işleminin nasıl uygulanacağını / uygulanabileceğini göstermek için oradadır, bu açıkça üretim kalite kodu değildir.
David Rodríguez - dribeas

2
@MooingDuck: Lambdaların kopyalanabilir olduğuna inanıyorum (5.1.2 / 19), ancak soru bu değil std::function, dahili nesne kopyalanırsa anlambiliminin doğru olup olmayacağı ve bunun böyle olacağını düşünmüyorum (Bir değeri yakalayan ve değiştirilebilir olan, a içinde saklanan bir lambda düşünün std::function, eğer fonksiyon durumu std::functionstandart bir algoritmanın içindeki kopya sayısı kopyalanmışsa, farklı sonuçlara neden olabilir ki bu istenmeyen bir durumdur.
David Rodríguez - dribeas

1
@ MiklósHomolya: g ++ 4.8 ile test ettim ve uygulama dahili durumu kopyalıyor. Çağrılabilir varlık dinamik bir tahsis gerektirecek kadar büyükse, kopyası std::functionbir tahsisatı tetikleyecektir.
David Rodríguez - dribeas

4
@ DavidRod Rodríguez-dribeas paylaşılan durumu istenmeyen bir durumdur, çünkü küçük nesne optimizasyonu, bir derleyici ve derleyici sürümü tarafından belirlenen boyut eşiğinde paylaşılan durumdan paylaşılmayan duruma geçeceğiniz anlamına gelir (küçük nesne optimizasyonu paylaşılan durumu engelleyeceği için). Sorunlu görünüyor.
Yakk - Adam Nevraumont

23

@David Rodríguez - dribeas'ın cevabı, tür silmeyi göstermek için iyidir, ancak tür silme aynı zamanda türlerin nasıl kopyalandığını da içerdiğinden yeterince iyi değildir (bu yanıtta işlev nesnesi kopyalanabilir olmayacaktır). Bu davranışlar function, işlev verilerinin yanı sıra nesnede de depolanır .

Ubuntu 14.04 gcc 4.8'den STL uygulamasında kullanılan hile, bir genel işlev yazmak, her olası işlev türü ile uzmanlaşmak ve bunları evrensel bir işlev işaretçisi türüne çevirmektir. Bu nedenle tip bilgisi silinir .

Bunun basitleştirilmiş bir versiyonunu kaldırdım. Umarım yardımcı olur

#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 sürümünde de bazı optimizasyonlar var

  • construct_fve destroy_fbazı bayt kaydetmek için (ne yapacağını söyler ek bir parametre ile birlikte) bir işlev işaretçisi içine karıştırılır
  • ham işaretçiler, functor nesnesini bir işlev işaretçisi ile birlikte depolamak için kullanılır union, böylece bir functionnesne bir işlev işaretçisinden oluşturulduğunda, unionyığın alanı yerine doğrudan depolanır.

Belki daha hızlı bir uygulama olduğunu duyduğum için STL uygulaması en iyi çözüm değildir . Ancak, temeldeki mekanizmanın aynı olduğuna inanıyorum.


20

Belirli argüman türleri için ("f'nin hedefi, aracılığıyla iletilen çağrılabilir bir nesne reference_wrapperveya bir işlev işaretçisi ise"), std::function'nin yapıcısı istisnalara izin vermez, bu nedenle dinamik bellek kullanımı söz konusu değildir. Bu durumda, tüm veriler doğrudan std::functionnesnenin içinde saklanmalıdır .

Genel durumda, (lambda durumu dahil), dinamik bellek (standart ayırıcı veya kurucuya geçirilen bir ayırıcı aracılığıyla std::function) kullanılarak uygulama uygun gördüğü için izin verilir. Standart, uygulamaların önlenebiliyorsa dinamik bellek kullanmamasını önerir, ancak haklı olarak söylediğiniz gibi, işlev nesnesi (nesne değil, std::functioniçine sarılmış nesne) yeterince büyükse, bunu engellemenin bir yolu yoktur, çünkü std::functionsabit bir boyuta sahiptir.

Bu istisna atma izni, hem normal kurucuya hem de kopya yapıcıya verilir; bu, kopyalama sırasında da dinamik bellek ayırmalarına oldukça açık bir şekilde izin verir. Hareketler için dinamik belleğin gerekli olması için hiçbir neden yoktur. Standart bunu açıkça yasaklamıyor gibi görünüyor ve eğer hareket sarılmış nesnenin türünün hareket yapıcısını çağırabilirse muhtemelen yapamaz, ancak hem uygulama hem de nesneleriniz mantıklıysa, hareket etmenin neden olmayacağını varsayabilmelisiniz. herhangi bir tahsis.


-6

Bir functor nesnesi yapmaya std::functionaşırı yükler operator(), lambda da aynı şekilde çalışır. Temel olarak, operator()işlevin içinden erişilebilen üye değişkenleri olan bir yapı oluşturur . Bu nedenle akılda tutulması gereken temel kavram, lambda'nın bir işlev değil, bir nesne (işlev veya işlev nesnesi olarak adlandırılır) olmasıdır. Standart, önlenebilirse dinamik bellek kullanmamayı söylüyor.


1
Rasgele büyük lambdalar nasıl sabit bir boyuta sığabilir std::function? Buradaki anahtar soru budur.
Miklós Homolya

2
@aaronman: Her std::functionnesnenin aynı boyutta olduğunu ve içerdiği lambdaların boyutu olmadığını garanti ederim .
Ördek mölemeye

5
@aaronman, her std::vector<T...> nesnenin gerçek ayırıcı örneğinden / eleman sayısından bağımsız bir (eş zaman) sabit boyuta sahip olması gibi.
sehe

3
@aaronman: Pekala, belki std :: function'ın rastgele boyutlandırılmış lambdalar içerebilecek şekilde nasıl uygulandığını yanıtlayan bir stackoverflow sorusu
bulmalısınız

1
@aaronman: Yapım aşamasında, atama sırasında çağrılabilir varlık ayarlandığında ... std::function<void ()> f;orada ayırmaya gerek yok, std::function<void ()> f = [&]() { /* captures tons of variables */ };büyük olasılıkla tahsis eder. std::function<void()> f = &free_function;muhtemelen ikisini de tahsis etmiyor ...
David Rodríguez - dribeas
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.