C ++ 11'in lambda'sı, varsayılan olarak, değere göre yakalama için “mutable” anahtar sözcüğünü gerektirir?


256

Kısa bir örnek:

#include <iostream>

int main()
{
    int n;
    [&](){n = 10;}();             // OK
    [=]() mutable {n = 20;}();    // OK
    // [=](){n = 10;}();          // Error: a by-value capture cannot be modified in a non-mutable lambda
    std::cout << n << "\n";       // "10"
}

Soru: mutableAnahtar kelimeye neden ihtiyacımız var ? Geleneksel parametrelerden adlandırılmış işlevlere geçişten oldukça farklıdır. Gerideki mantık nedir?

Ben değere göre ele geçirmenin tüm noktasının, kullanıcının geçici olarak değiştirmesine izin vermek olduğu izlenimi altındaydım - aksi halde, her zaman referansla yakala özelliğini kullanmaktan daha iyiyim, değil mi?

Aydınlanma var mı?

(Bu arada MSVC2010 kullanıyorum. AFAIK bu standart olmalı)


101
İyi soru; bir şey nihayet constvarsayılan olarak sevindim rağmen !
xtofl

3
Cevap değil, ama bence bu mantıklı bir şey: eğer değere göre bir şey alırsanız, sadece yerel bir değişkene 1 kopya kaydetmek için değiştirmemelisiniz. En azından = 'yi & ile değiştirerek n'yi değiştirme hatasını yapmayacaksınız.
stefaanv

8
@xtofl: Diğer her şey constvarsayılan olarak olmadığında iyi olduğundan emin değilim .
kizzx2

8
@ Tamás Szelei: Bir tartışma başlatmak için değil, IMHO'nun "öğrenmesi kolay" kavramının C ++ dilinde, özellikle modern günlerde yeri yok. Neyse: P
kizzx2

3
"değere göre yakalamanın bütün noktası, kullanıcının geçici olanı değiştirmesine izin vermektir" - Hayır, bütün mesele, lambda'nın yakalanan değişkenlerin ömrünün ötesinde geçerli kalabilmesidir. C ++ lambdalar sadece ref tarafından yakalansaydı, çok fazla senaryoda kullanılamazlardı.
Sebastian Redl

Yanıtlar:


230

Bu gerektiren mutablevarsayılan olarak bir işlev nesne aynı sonucu denir her zaman üretmelidir çünkü. Bu, nesne yönelimli bir işlev ile genel bir değişken kullanan bir işlev arasındaki farktır.


7
Bu iyi bir nokta. Tamamen katılıyorum. C ++ 0x olsa da, ben varsayılan nasıl yukarıdaki zorlamak yardımcı görmüyorum. Lambda'nın alıcı ucunda olduğumu düşünün, örneğin öyleyim void f(const std::function<int(int)> g). gAslında referans olarak şeffaf olduğunu nasıl garanti ederim ? gtedarikçisinin mutablezaten kullandığı olabilir . Bu yüzden bilmeyeceğim. Varsayılan olmayan ise Öte yandan, constve insanlar eklemelisiniz constyerine mutablefonksiyon nesnelere, derleyici aslında zorlayabilir const std::function<int(int)>şimdi bölümünü ve fvarsayabiliriz golduğunu const, hayır?
kizzx2

8
@ kizzx2: C ++ 'da hiçbir şey zorunlu değildir , sadece önerilir. Her zamanki gibi, aptalca bir şey yaparsanız (referans şeffaflık için belgelenmiş gereksinim ve daha sonra referans olmayan şeffaf işlevden geçerseniz), size ne gelirse onu alırsınız.
Köpek yavrusu

6
Bu cevap gözlerimi açtı. Daha önce, bu durumda lambda'nın sadece geçerli "çalışma" için bir kopyasını değiştirdiğini düşünmüştüm.
Zsolt Szatmari

4
@ZsoltSzatmari Yorumunuz gözlerimi açtı! : -DI yorumunuzu okuyana kadar bu cevabın gerçek anlamını alamadım.
Jendas

5
Bu cevabın temel önermesine katılmıyorum. C ++, dilde başka hiçbir yerde "işlevler her zaman aynı değeri döndürmelidir" kavramına sahip değildir. Bir tasarım prensibi olarak, bir fonksiyon yazmak için iyi bir yol kabul edeceğini, ama bunun yanı su tutar sanmıyorum standart davranış nedeni.
Ionoclast Brigham

103

Kodunuz neredeyse buna eşittir:

#include <iostream>

class unnamed1
{
    int& n;
public:
    unnamed1(int& N) : n(N) {}

    /* OK. Your this is const but you don't modify the "n" reference,
    but the value pointed by it. You wouldn't be able to modify a reference
    anyway even if your operator() was mutable. When you assign a reference
    it will always point to the same var.
    */
    void operator()() const {n = 10;}
};

class unnamed2
{
    int n;
public:
    unnamed2(int N) : n(N) {}

    /* OK. Your this pointer is not const (since your operator() is "mutable" instead of const).
    So you can modify the "n" member. */
    void operator()() {n = 20;}
};

class unnamed3
{
    int n;
public:
    unnamed3(int N) : n(N) {}

    /* BAD. Your this is const so you can't modify the "n" member. */
    void operator()() const {n = 10;}
};

int main()
{
    int n;
    unnamed1 u1(n); u1();    // OK
    unnamed2 u2(n); u2();    // OK
    //unnamed3 u3(n); u3();  // Error
    std::cout << n << "\n";  // "10"
}

Böylece lambdas, değişebilir olduğunu söylemediğiniz sürece const varsayılan olarak varsayılan bir operatör () ile bir sınıf oluşturmak olarak düşünebilirsiniz.

Ayrıca [] içinde yakalanan tüm değişkenleri (açık veya kapalı olarak) bu sınıfın üyeleri olarak düşünebilirsiniz: [=] için nesnelerin kopyalarını veya [&] için nesnelere referanslar. Lambda'nızı gizli bir kurucu varmış gibi ilan ettiğinizde başlatılırlar.


5
Güzel bir açıklama yaparken neyi bir constveya mutablelambda eşdeğer kullanıcı tanımlı türleri olarak uygulanması durumunda nasıl görüneceğini, soru (başlık olarak ve yorumlarda OP tarafından hazırlanan) 'dir neden const bu cevap vermez, böylece varsayılan vardır.
underscore_d

36

Ben değere göre ele geçirmenin tüm noktasının, kullanıcının geçici olarak değiştirmesine izin vermek olduğu izlenimi altındaydım - aksi halde, her zaman referansla yakala özelliğini kullanmaktan daha iyiyim, değil mi?

Soru şu, "neredeyse" mi? Sık kullanılan bir kullanım durumunun lambdaları iade etmek veya geçmek gibi görünmektedir:

void registerCallback(std::function<void()> f) { /* ... */ }

void doSomething() {
  std::string name = receiveName();
  registerCallback([name]{ /* do something with name */ });
}

Bence bu mutable"neredeyse" bir durum değil. "Değere göre yakala" gibi "yakalanan varlık öldükten sonra değerini kullanmama izin ver" yerine "bir kopyasını değiştirmeme izin ver" olarak değerlendiriyorum. Ama belki de bu tartışılabilir.


2
İyi örnek. Bu, değere göre yakalamanın kullanımı için çok güçlü bir kullanım durumudur. Ama neden varsayılan olarak const? Hangi amaca ulaşıyor? mutablezaman, burada yersiz görünüyor constolduğunu değil (: P) başka dilin her şey "neredeyse" varsayılan.
kizzx2

8
@ kizzx2: Keşke constvarsayılan olsaydı , en azından insanlar const doğruluğunu düşünmek zorunda kalacaktı: /
Matthieu M.

1
@ kizzx2 lambda kağıtlarına baktığımda, bana varsayılan olarak ayarladıkları anlaşılıyor, constböylece lambda nesnesinin const olup olmadığını söyleyebilirler. Örneğin, a alan bir fonksiyona aktarabilirlerdi std::function<void()> const&. Lambda'nın yakalanan kopyalarını değiştirmesine izin vermek için ilk makalelerde kapağın veri üyeleri mutabledahili olarak otomatik olarak tanımlandı . Şimdi mutablelambda ifadesini elle koymak zorundasınız . Yine de ayrıntılı bir gerekçe bulamadım.
Johannes Schaub - litb


5
Bu noktada, bana göre, "gerçek" cevap / mantık "bir uygulama detayı üzerinde çalışamadılar" gibi görünüyor: /
kizzx2 16

32

C ++ standardizasyon komitesinin tanınmış bir üyesi olan Herb Sutter, FWIW, Lambda Düzeltme ve Kullanılabilirlik Sorunlarında bu soruya farklı bir cevap veriyor :

Programcının yerel bir değişkeni değere göre yakaladığı ve yakalanan değeri (lambda nesnesinin bir üye değişkeni olan) değiştirmeye çalıştığı bu hasır adam örneğini düşünün:

int val = 0;
auto x = [=](item e)            // look ma, [=] means explicit copy
            { use(e,++val); };  // error: count is const, need ‘mutable’
auto y = [val](item e)          // darnit, I really can’t get more explicit
            { use(e,++val); };  // same error: count is const, need ‘mutable’

Bu özellik, kullanıcının bir kopyasını aldığını fark etmeyebileceği ve özellikle lambdaların kopyalanabilmesi nedeniyle farklı bir lambda kopyasını değiştirebileceği endişesinden eklenmiş gibi görünüyor.

Makalesi, bunun neden C ++ 14'te değiştirilmesi gerektiğiyle ilgili. Bu özel özellik ile ilgili olarak "[komite üyesi] zihinlerde ne olduğunu" bilmek istiyorsanız, kısa, iyi yazılmış, okumaya değer.


16

Lambda fonksiyonunuzun kapanma tipinin ne olduğunu düşünmeniz gerekir . Bir Lambda ifadesini her bildirdiğinizde, derleyici, niteliklerle ( bildirilen Lambda ifadesinin bulunduğu ortam ) ve işlev çağrısıyla adlandırılmamış bir sınıf bildiriminden daha az bir şey olmayan bir kapatma türü oluşturur , yakalanan varlık, kapatmanızın bir özelliği haline gelir yazın. Bu, değer tarafından yakalanan değişken değişkente yapılan değişikliklerin üst kapsama yayılmamasına, ancak durumsal Lambda'nın içinde kalmasına neden olan şeydir. Her zaman bana çok yardımcı olan Lambda ifadenizin ortaya çıkan kapanış tipini hayal etmeye çalışın ve umarım size de yardımcı olabilir.::operator() uygulandığı . Kullandığınız bir değişken yakaladığınızda kopyasını değer-ile- , derleyici yeni yaratacak constsen Lambda ifade içinde bunu değiştiremezsiniz bu yüzden sebebi bir "salt okunur" özelliği, çünkü kapatma türü niteliğini onlar " kapatma " deyin , çünkü bir şekilde, değişkenleri üst kapsamdan Lambda kapsamına kopyalayarak Lambda ifadenizi kapatıyorsunuz.mutablenon-const


14

5.1.2 [expr.prim.lambda], alt madde 5 uyarınca bu taslağa bakınız :

Bir lambda ifadesi için kapatma tipinde, parametreleri ve dönüş tipi sırasıyla lambda-ifadesinin parametre-bildirim-cümlecik ve izleyen dönüş tipi tarafından açıklanan genel bir satır içi işlev çağrısı operatörü (13.5.4) vardır. Bu işlev çağrısı işleci, yalnızca lambdaexpression'un parametre bildirimi yan tümcesini değiştirilebiliyorsa const (9.3.1) olarak bildirilir.

Litb'nin yorumunda düzenleyin: Değişkenlerde yapılan dış değişikliklerin lambda içine yansımaması için belki de değere göre yakalamayı düşündüler? Referanslar her iki şekilde de çalışır, bu yüzden bu benim açıklamam. Yine de iyi olup olmadığını bilmiyorum.

Kizzx2'nin yorumunda düzenleme: Bir lambda'nın en çok ne zaman kullanılacağı, algoritmalar için bir işlev olarak kullanılır. Varsayılan constness, normal constkalifiye işlevlerin orada kullanılabileceği gibi sabit bir ortamda kullanılmasına izin verir , ancak constkalifiye olmayanlar kullanamaz. Belki de sadece akıllarında neler olduğunu bilen vakalar için daha sezgisel hale getirmeyi düşündüler. :)


Bu standart, ama neden bu şekilde yazdılar?
kizzx2

@ kizzx2: Açıklamam doğrudan bu teklifin altında. :) Litb'nin yakalanan nesnelerin ömrü hakkında söyledikleriyle biraz ilgilidir, ancak biraz daha ileri gider.
Xeo

@Xeo: Ah evet, bunu kaçırdım: P Aynı zamanda değere göre yakalamanın iyi bir şekilde kullanılması için iyi bir açıklama . Ama neden constvarsayılan olarak olmalı ? Zaten yeni bir kopyam var, değiştirmeme izin vermemek garip görünüyor - özellikle de bununla ilgili yanlış bir şey değil - sadece eklememi istiyorlar mutable.
kizzx2

Yeni bir genel işlev bildirimi sözdizimi yaratma girişimi olduğuna inanıyorum. Ayrıca, her şeyi varsayılan olarak sabitleyerek diğer sorunları da düzeltmesi gerekiyordu. Asla tamamlanmadı, ama fikirler lambda tanımına yapıştı.
Bo Persson

2
@ kizzx2 - Her şeye yeniden başlayabilirsek, büyük olasılıkla vardeğişime izin veren bir anahtar kelime olarak ve her şey için varsayılan olarak sabit kalmamız gerekir . Şimdi yapmıyoruz, bu yüzden bununla yaşamak zorundayız. IMO, C ++ 2011 her şeyi göz önüne alarak oldukça iyi çıktı.
Bo Persson

11

Ben değere göre ele geçirmenin tüm noktasının, kullanıcının geçici olarak değiştirmesine izin vermek olduğu izlenimi altındaydım - aksi halde, her zaman referansla yakala özelliğini kullanmaktan daha iyiyim, değil mi?

nolduğu değil geçici. n, lambda ifadesi ile oluşturduğunuz lambda-function-object öğesinin bir üyesidir. Varsayılan beklenti, lambda'nızı çağırmanın durumunu değiştirmemesidir, bu nedenle yanlışlıkla değiştirmenizi önlemek için sabittir n.


1
Tüm lambda nesnesi geçicidir, üyelerinin de geçici ömrü vardır.
Ben Voigt

2
@Ben: IIRC, birisi "geçici" dediğinde, onu lambda'nın kendisi olan isimsiz geçici nesne anlamına geldiğini anlıyordum , ama üyeleri değil. Ve ayrıca lambda'nın "içinden", lambda'nın kendisinin geçici olup olmadığı gerçekten önemli değil. Soruyu yeniden okumak, OP'nin "geçici" dediğinde "lambda içinde n" demek anlamına geldiği anlaşılıyor.
Martin Ba

6

Yakalamanın ne anlama geldiğini anlamalısınız! tartışmayı geçmeyi değil! bazı kod örneklerine bakalım:

int main()
{
    using namespace std;
    int x = 5;
    int y;
    auto lamb = [x]() {return x + 5; };

    y= lamb();
    cout << y<<","<< x << endl; //outputs 10,5
    x = 20;
    y = lamb();
    cout << y << "," << x << endl; //output 10,20

}

Gördüğünüz gibi lambda xdeğiştirilmiş olsa bile 20hala 10 geri dönüyor ( xhala 5lambda xiçinde ) Lambda'nın içinde değiştirmek, her çağrıda lambda'nın kendisini değiştirmek anlamına gelir (lambda her çağrıda mutasyona uğramaktadır). Doğruluğu sağlamak için standart mutableanahtar kelimeyi tanıttı . Bir lambda'yı değiştirilebilir olarak belirterek, lambda'ya yapılan her çağrının lambda'nın kendisinde bir değişikliğe neden olabileceğini söylüyorsunuz. Başka bir örnek görelim:

int main()
{
    using namespace std;
    int x = 5;
    int y;
    auto lamb = [x]() mutable {return x++ + 5; };

    y= lamb();
    cout << y<<","<< x << endl; //outputs 10,5
    x = 20;
    y = lamb();
    cout << y << "," << x << endl; //outputs 11,20

}

Yukarıdaki örnek, lambda'yı değişken hale getirerek, lambda'nın xiçinde değiştirilmesinin , her çağrıda lambda'yı yeni bir değerle "değiştirdiğini" , ana işlevdeki xgerçek değerle ilgisi olmadığını xgöstermektedir.


4

Artık mutablelambda deklarasyonlarındaki ihtiyacı hafifletmek için bir teklif var : n3424


Bunun ne olduğuna dair herhangi bir bilgi var mı? Kişisel olarak bunun kötü bir fikir olduğunu düşünüyorum, çünkü yeni “keyfi ifadelerin yakalanması” acı noktalarının çoğunu düzeltir.
Ben Voigt

1
@BenVoigt Evet, değişim uğruna bir değişiklik gibi görünüyor.
Miles Rout

3
@BenVoigt Adil olmakla birlikte, muhtemelen C ++ ' mutableda bir anahtar kelime olduğunu bilmeyen birçok C ++ geliştiricisi olduğunu umuyorum .
Miles Rout

1

Yavru köpeğin cevabını genişletmek için lambda fonksiyonlarının saf fonksiyonlar olması amaçlanmıştır . Bu, benzersiz bir giriş seti verilen her çağrının daima aynı çıkışı döndürdüğü anlamına gelir. Girdiyi , lambda çağrıldığında tüm bağımsız değişkenlerin yanı sıra yakalanan tüm değişkenler kümesi olarak tanımlayalım .

Saf fonksiyonlarda çıkış, sadece bazı dahili durumlara değil, yalnızca girdilere bağlıdır. Bu nedenle, safsa herhangi bir lambda fonksiyonunun durumunu değiştirmesi gerekmez ve bu nedenle değişmezdir.

Bir lambda referans olarak yakalandığında, yakalanan değişkenler üzerine yazma saf fonksiyon kavramı üzerinde bir zorlamadır, çünkü saf bir fonksiyonun tek yapması gereken bir çıktı döndürmektir, ancak lambda mutasyona uğramaz çünkü yazma harici değişkenlere olur. Bu durumda bile doğru bir kullanım, lambda tekrar aynı girişle çağrılırsa, referansın değişkenleri üzerindeki bu yan etkilere rağmen, çıkışın her zaman aynı olacağı anlamına gelir. Bu tür yan etkiler sadece bazı ek girdileri (örneğin bir sayacı güncelleme) döndürmenin yoludur ve örneğin tek bir değer yerine bir demet döndürerek saf bir işleve dönüştürülebilir.

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.