C ++ 11 lambda uygulaması ve bellek modeli


97

C ++ 11 kapanışlarının nasıl doğru bir şekilde düşünülebileceği std::functionve nasıl uygulandığı ve belleğin nasıl işlendiği konusunda biraz bilgi almak istiyorum .

Erken optimizasyona inanmasam da, yeni kod yazarken seçimlerimin performansa etkisini dikkatlice düşünme alışkanlığım var. Ayrıca, mikrodenetleyicilerde ve deterministik olmayan bellek tahsisi / serbest bırakma duraklamalarının önleneceği ses sistemleri için makul miktarda gerçek zamanlı programlama yapıyorum.

Bu nedenle, C ++ lambdaları ne zaman kullanıp kullanmama konusunda daha iyi bir anlayış geliştirmek istiyorum.

Şu anki anlayışım, yakalanan kapanışı olmayan bir lambda'nın tam olarak bir C geri araması gibi olduğudur. Bununla birlikte, ortam değere göre veya referans olarak yakalandığında, yığın üzerinde anonim bir nesne oluşturulur. Bir işlevden bir değer kapanışı döndürülmesi gerektiğinde, onu içine alır std::function. Bu durumda kapanış hafızasına ne olur? Yığından yığına kopyalanıyor mu? Ne zaman serbest std::functionbırakılırsa, yani a gibi referans sayılır std::shared_ptrmı?

Gerçek zamanlı bir sistemde, B'yi devam argümanı olarak A'ya geçirerek bir lambda işlevleri zinciri kurabileceğimi ve böylece bir işlem hattının A->Byaratılacağını hayal ediyorum . Bu durumda, A ve B kapanışları bir kez tahsis edilecektir. Bunların yığına mı yoksa yığına mı tahsis edileceğinden emin değilim. Ancak genel olarak bu, gerçek zamanlı bir sistemde kullanmak için güvenli görünmektedir. Öte yandan, eğer B, döndürdüğü bazı lambda C fonksiyonunu inşa ederse, o zaman C için bellek tekrar tekrar tahsis edilir ve serbest bırakılır, bu da gerçek zamanlı kullanım için kabul edilemez.

Sözde kodda, gerçek zamanlı güvenli olacağını düşündüğüm bir DSP döngüsü. A bloğunu ve ardından A'nın argümanını çağırdığı B bloğunu gerçekleştirmek istiyorum. Bu işlevlerin her ikisi de std::functionnesneleri döndürür , dolayısıyla ortamının öbek üzerinde depolandığı fbir std::functionnesne olacaktır :

auto f = A(B);  // A returns a function which calls B
                // Memory for the function returned by A is on the heap?
                // Note that A and B may maintain a state
                // via mutable value-closure!
for (t=0; t<1000; t++) {
    y = f(t)
}

Ve gerçek zamanlı kodda kullanmanın kötü olabileceğini düşündüğüm bir şey:

for (t=0; t<1000; t++) {
    y = A(B)(t);
}

Ve yığın hafızasının muhtemelen kapanış için kullanıldığını düşündüğüm yer:

freq = 220;
A = 2;
for (t=0; t<1000; t++) {
    y = [=](int t){ return sin(t*freq)*A; }
}

İkinci durumda, kapanma döngünün her yinelemesinde oluşturulur, ancak önceki örnekten farklı olarak ucuzdur, çünkü bu sadece bir işlev çağrısı gibidir, yığın tahsisleri yapılmaz. Dahası, bir derleyicinin kapanışı "kaldırıp kaldırmayacağını" ve satır içi optimizasyonlar yapıp yapamayacağını merak ediyorum.

Bu doğru mu? Teşekkür ederim.


4
Lambda ifadesi kullanırken ek yük yoktur. Diğer seçenek, böyle bir işlev nesnesini kendiniz yazmak olacaktır, bu da tamamen aynı olacaktır. Btw, satır içi soruda, derleyici ihtiyaç duyduğu tüm bilgilere sahip olduğundan, kesinlikle operator(). Yapılması gereken bir "kaldırma" yoktur, lambdalar özel bir şey değildir. Yerel bir işlev nesnesi için sadece kısa bir eldir.
Xeo

Bu std::function, durumunu yığın üzerinde depolayıp depolamadığıyla ilgili bir soru gibi görünüyor ve lambdalarla ilgisi yok. Bu doğru mu?
Mooing Duck

8
Sadece herhangi bir yanlış anlamalar söz konusu heceleyerek: Bir lambda ifadesi değil bir std::function!!
Xeo

1
Yalnızca bir yan yorum: Bir işlevden bir lambda döndürürken dikkatli olun, çünkü referans tarafından yakalanan herhangi bir yerel değişken, lambdayı oluşturan işlevden ayrıldıktan sonra geçersiz hale gelir.
Giorgio

2
@Steve, C ++ 14'ten beri, autodönüş türüne sahip bir işlevden bir lambda döndürebilirsiniz.
Oktalist

Yanıtlar:


104

Şu anki anlayışım, yakalanan kapanışı olmayan bir lambda'nın tam olarak bir C geri araması gibi olduğudur. Bununla birlikte, ortam ya değere ya da referansa göre yakalandığında, yığın üzerinde anonim bir nesne oluşturulur.

Hayır; o zaman yığın üzerinde oluşturulan bilinmeyen tip C ++ nesnesi. Yakalamasız bir lambda bir işlev işaretçisine dönüştürülebilir (ancak C çağrı kuralları için uygun olup olmadığı uygulamaya bağlıdır), ancak bu onun bir işlev işaretçisi olduğu anlamına gelmez .

Bir işlevden bir değer kapanışı döndürülmesi gerektiğinde, onu std :: function içinde sarar. Bu durumda kapanış hafızasına ne olur?

Lambda, C ++ 11'de özel bir şey değildir. Diğer nesneler gibi bir nesne. Lambda ifadesi, yığındaki bir değişkeni başlatmak için kullanılabilen geçici bir sonuç verir:

auto lamb = []() {return 5;};

lambbir yığın nesnesidir. Yapıcı ve yıkıcı vardır. Ve bunun için tüm C ++ kurallarına uyacaktır. Türü lambyakalanan değerleri / referansları içerecektir; tıpkı diğer herhangi bir türdeki diğer nesne üyeleri gibi, o nesnenin üyeleri olacaklar.

Şunlara verebilirsiniz std::function:

auto func_lamb = std::function<int()>(lamb);

Bu durumda, değerinin bir kopyasını alırlamb . Bir lambşeyi değerine göre ele geçirmiş olsaydı, bu değerlerin iki kopyası olurdu; biri giriş lambve biri giriş func_lamb.

Mevcut kapsam bittiğinde, yığın değişkenlerini temizleme kurallarına func_lambgöre yok edilecek ve ardından lambkaldırılacaktır.

Bir tanesini yığın üzerinde kolayca ayırabilirsiniz:

auto func_lamb_ptr = new std::function<int()>(lamb);

Tam olarak bir std::functiongo içeriğinin belleğinin uygulamaya bağlı olduğu, ancak kullanılan tür silme işlemi std::functiongenellikle en az bir bellek tahsisi gerektirdiğinde. Bu yüzden std::functionkurucu bir ayırıcı alabilir.

Std :: işlevi serbest bırakıldığında serbest bırakılır mı, yani std :: shared_ptr gibi başvuru sayılır mı?

std::functioniçeriğinin bir kopyasını saklar . Hemen hemen her standart kitaplık C ++ türü gibi function, değer semantiğini kullanır . Böylece kopyalanabilir; kopyalandığında yeni functionnesne tamamen ayrıdır. Ayrıca taşınabilir olduğu için, herhangi bir dahili tahsis, daha fazla ayırma ve kopyalamaya gerek kalmadan uygun şekilde aktarılabilir.

Böylelikle referans saymaya gerek yoktur.

"Bellek ayırma" nın "gerçek zamanlı kodda kullanım için kötü" olduğunu varsayarak belirttiğiniz diğer her şey doğrudur.


1
Mükemmel açıklama, teşekkür ederim. Yani oluşturulması std::function, hafızanın tahsis edildiği ve kopyalandığı noktadır. Öyle görünüyor ki, ilk önce a std::function, evet?
Steve

3
@Steve: Evet; kapsamdan çıkması için bir tür konteynere bir lambda sarmanız gerekir.
Nicol Bolas

İşlevin kodunun tamamı kopyalanmış mı, yoksa orijinal işlev derleme zamanı ayrılmış mı ve kapalı değerleri geçirmiş mi?
Llamageddon

Standardın aşağı yukarı dolaylı olarak (§ 20.8.11.2.1 [func.wrap.func.con] ¶ 5) bir lambda herhangi bir şey yakalamaması durumunda std::functiondinamik bellek olmadan bir nesnede depolanabileceğini zorunlu kıldığını eklemek istiyorum. tahsis devam ediyor.
5gon12eder

2
@Yakk: "Büyük" kelimesini nasıl tanımlarsınız? İki durum göstergesine sahip bir nesne "büyük" midir? Peki ya 3 ya da 4? Ayrıca, tek sorun nesne boyutu değildir; Nesne nothrow-hareketli değil ise, bu gerekir , çünkü bir ayırma saklanabilir functionbir noexcept hareket kurucu sahiptir. "Genel olarak gerektirir" demenin tüm amacı, " her zaman gerektirir" demememdir: hiçbir tahsisin yapılmayacağı durumlar vardır.
Nicol Bolas

1

C ++ lambda, aşırı yüklenmiş (anonim) bir Functor sınıfı etrafında sadece sözdizimsel bir şekerdir operator()ve std::functionsadece çağrılabilirler (yani functors, lambdas, c-functions, ...) etrafında bir sarmalayıcıdır ve mevcut "katı lambda nesnesini" değere göre kopyalar. yığın kapsamı - yığına .

Gerçek kurucuların / relocatonların sayısını test etmek için bir test yaptım (başka bir düzeyde shared_ptr'ye sarma kullanarak ama durum böyle değil). Kendin için gör:

#include <memory>
#include <string>
#include <iostream>

class Functor {
    std::string greeting;
public:

    Functor(const Functor &rhs) {
        this->greeting = rhs.greeting;
        std::cout << "Copy-Ctor \n";
    }
    Functor(std::string _greeting="Hello!"): greeting { _greeting } {
        std::cout << "Ctor \n";
    }

    Functor & operator=(const Functor & rhs) {
        greeting = rhs.greeting;
        std::cout << "Copy-assigned\n";
        return *this;
    }

    virtual ~Functor() {
        std::cout << "Dtor\n";
    }

    void operator()()
    {
        std::cout << "hey" << "\n";
    }
};

auto getFpp() {
    std::shared_ptr<std::function<void()>> fp = std::make_shared<std::function<void()>>(Functor{}
    );
    (*fp)();
    return fp;
}

int main() {
    auto f = getFpp();
    (*f)();
}

şu çıktıyı yapar:

Ctor 
Copy-Ctor 
Copy-Ctor 
Dtor
Dtor
hey
hey
Dtor

Yığın-ayrılmış lambda nesnesi için tam olarak aynı ctors / dtors kümesi çağrılacaktır! (Şimdi yığın tahsisi için Ctor'u, std :: function'da inşa etmek için Copy-ctor'u (+ heap ayırma) ve shared_ptr öbek tahsisi + işlevin inşasını yapmak için bir tane daha çağırır)

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.