Dlaczego domyślnie lambda C ++ 11 wymaga słowa kluczowego „mutable” do przechwytywania według wartości?


256

Krótki przykład:

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

Pytanie: Dlaczego potrzebujemy mutablesłowa kluczowego? Różni się to od tradycyjnego przekazywania parametrów do nazwanych funkcji. Jakie jest uzasadnienie?

Miałem wrażenie, że celem przechwytywania według wartości jest umożliwienie użytkownikowi zmiany ustawienia tymczasowego - w przeciwnym razie prawie zawsze lepiej jest używać przechwytywania przez odniesienie, prawda?

Jakieś oświecenia?

(Nawiasem mówiąc, używam MSVC2010. AFAIK to powinno być standardowe)


101
Dobre pytanie; chociaż cieszę się, że coś jest wreszcie constdomyślnie!
xtofl

3
Nie jest to odpowiedź, ale myślę, że jest to rozsądna rzecz: jeśli weźmiesz coś według wartości, nie powinieneś zmieniać go tylko po to, aby zapisać ci 1 kopię do zmiennej lokalnej. Przynajmniej nie pomylisz się, zmieniając n, zastępując = przez &.
stefanow

8
@xtofl: Nie jestem pewien, czy to dobrze, gdy wszystko inne nie constjest domyślnie.
kizzx2

8
@ Tamás Szelei: Nie rozpoczynać kłótni, ale w IMHO koncepcja „łatwa do nauczenia” nie ma miejsca w języku C ++, szczególnie w dzisiejszych czasach. W każdym razie: P
kizzx2

3
„celem przechwytywania według wartości jest umożliwienie użytkownikowi zmiany wartości tymczasowej” - Nie, chodzi o to, że lambda może pozostać ważna przez cały okres istnienia dowolnych przechwyconych zmiennych. Gdyby lambda C ++ miały tylko przechwytywanie przez odniesienie, byłyby bezużyteczne ze względu na zbyt wiele scenariuszy.
Sebastian Redl,

Odpowiedzi:


230

Wymaga to, mutableponieważ domyślnie obiekt funkcyjny powinien generować ten sam wynik przy każdym wywołaniu. Jest to różnica między funkcją zorientowaną obiektowo a funkcją efektywnie wykorzystującą zmienną globalną.


7
To dobra uwaga. W pełni się zgadzam. Jednak w C ++ 0x nie do końca rozumiem, jak domyślna pomaga wymusić powyższe. Rozważ, że jestem po stronie odbierającej lambda, np void f(const std::function<int(int)> g). Jestem . W jaki sposób mam zagwarantowane, że gjest to tak naprawdę przejrzyste odniesienie ? gdostawca mógł i mutabletak skorzystać . Więc nie będę wiedział. Z drugiej strony, jeśli domyślny jest nie- const, a ludzie muszą dodać constzamiast mutabledo obiektów funkcyjnych, kompilator może faktycznie egzekwować const std::function<int(int)>część i teraz fmożna zakładać, że gjest const, nie?
kizzx2

8
@ kizzx2: W C ++ nic nie jest wymuszone , tylko sugerowane. Jak zwykle, jeśli zrobisz coś głupiego (udokumentowane wymaganie przejrzystości referencyjnej, a następnie przejdziesz funkcję niereferencyjnie przezroczystą), dostaniesz wszystko, co do ciebie przyjdzie.
Szczeniak

6
Ta odpowiedź otworzyła mi oczy. Wcześniej myślałem, że w tym przypadku lambda mutuje tylko kopię dla bieżącego „uruchomienia”.
Zsolt Szatmari,

4
@ZsoltSzatmari Twój komentarz otworzył mi oczy! : -Da nie zrozumiałem prawdziwego znaczenia tej odpowiedzi, dopóki nie przeczytałem twojego komentarza.
Jendas,

5
Nie zgadzam się z podstawową przesłanką tej odpowiedzi. C ++ nie ma pojęcia „funkcje powinny zawsze zwracać tę samą wartość” w dowolnym innym miejscu w języku. Jako zasady projektowania, zgadzam się, że to dobry sposób, aby napisać funkcję, ale nie sądzę, że posiada wody jak na powód standardowego zachowania.
Ionoclast Brigham

103

Twój kod jest prawie równoważny z tym:

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

Możesz więc pomyśleć o lambdach jako o generowaniu klasy z operatorem (), która domyślnie przyjmuje wartość const, chyba że powiesz, że jest zmienna.

Możesz również myśleć o wszystkich zmiennych przechwyconych wewnątrz [] (jawnie lub niejawnie) jako członkach tej klasy: kopie obiektów dla [=] lub odniesienia do obiektów dla [&]. Są inicjowane, kiedy deklarujesz swoją lambda, jakby istniał ukryty konstruktor.


5
Podczas gdy ładne wyjaśnienie, jak wyglądałaby a constlub mutablelambda, gdyby zostały zaimplementowane jako równoważne typy zdefiniowane przez użytkownika, pytanie brzmi (jak w tytule i opracowane przez OP w komentarzach), dlaczego const jest domyślne, więc nie odpowiada na to pytanie.
underscore_d

36

Miałem wrażenie, że celem przechwytywania według wartości jest umożliwienie użytkownikowi zmiany ustawienia tymczasowego - w przeciwnym razie prawie zawsze lepiej jest używać przechwytywania przez odniesienie, prawda?

Pytanie brzmi, czy to „prawie”? Częstym przypadkiem użycia wydaje się być zwrot lub przekazanie lambdas:

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

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

Myślę, że mutableto nie jest przypadek „prawie”. Uważam, że „przechwytywanie według wartości” jak „pozwala mi używać jego wartości po śmierci przechwyconego bytu”, a nie „zezwalam na zmianę jego kopii”. Ale może to można argumentować.


2
Dobry przykład. Jest to bardzo silny przypadek użycia w przypadku przechwytywania według wartości. Ale dlaczego domyślnie const? Jaki cel to osiąga? mutablewydaje się nie na miejscu, gdy nieconst jest domyślnym w „prawie” (: P) całej reszcie języka.
kizzx2

8
@ kizzx2: Chciałbym, żeby constbyło to ustawienie domyślne, przynajmniej ludzie byliby zmuszeni rozważyć stałą poprawność: /
Matthieu M.

1
@ kizzx2 patrząc na dokumenty lambda, wydaje mi się, że domyślnie ustawiają, aby constmogli nazwać to, czy obiekt lambda jest const. Na przykład mogą przekazać go do funkcji przyjmującej std::function<void()> const&. Aby umożliwić lambdzie zmianę przechwyconych kopii, w początkowych dokumentach członkowie danych zamknięcia zostali zdefiniowani mutablewewnętrznie automatycznie. Teraz musisz ręcznie wprowadzić mutablewyrażenie lambda. Nie znalazłem jednak szczegółowego uzasadnienia.
Johannes Schaub - litb


5
W tym momencie dla mnie „prawdziwą” odpowiedzią / uzasadnieniem wydaje się być „nie udało się obejść szczegółów implementacji”: /
kizzx2

32

FWIW, Herb Sutter, znany członek komitetu normalizacyjnego C ++, podaje inną odpowiedź na to pytanie w kwestiach poprawności i użyteczności lambda :

Rozważmy przykład słomianego człowieka, w którym programista przechwytuje zmienną lokalną według wartości i próbuje zmodyfikować przechwyconą wartość (która jest zmienną składową obiektu lambda):

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’

Wydaje się, że ta funkcja została dodana z obawy, że użytkownik może nie zdawać sobie sprawy, że dostał kopię, aw szczególności, że skoro lambd można kopiować, może zmieniać inną kopię lambdy.

Jego artykuł mówi o tym, dlaczego należy to zmienić w C ++ 14. Jest krótki, dobrze napisany, wart przeczytania, jeśli chcesz wiedzieć „co jest w umyśle [członka komitetu]” w odniesieniu do tej konkretnej funkcji.


16

Musisz pomyśleć, jaki jest typ zamknięcia twojej funkcji Lambda. Za każdym razem, gdy deklarujesz wyrażenie Lambda, kompilator tworzy typ zamknięcia, który jest niczym innym jak nienazwaną deklaracją klasy z atrybutami ( środowisko, w którym deklarowane jest wyrażenie Lambda) i ::operator()zaimplementowane wywołanie funkcji . Podczas przechwytywania zmiennej przy użyciu funkcji kopiowania według wartości kompilator utworzy nowy constatrybut w typie zamknięcia, więc nie można go zmienić w wyrażeniu Lambda, ponieważ jest to atrybut „tylko do odczytu”, dlatego nazwij to „ zamknięciem ”, ponieważ w pewien sposób zamykasz wyrażenie Lambda, kopiując zmienne z górnego zakresu do zakresu Lambda.mutable, przechwycona jednostka stanie się non-constatrybutem typu zamknięcia. To powoduje, że zmiany dokonane w zmiennej zmiennej przechwytywanej przez wartość nie są propagowane do górnego zakresu, ale pozostają w stanie Lambda. Zawsze próbuj sobie wyobrazić wynikowy typ zamknięcia twojego wyrażenia Lambda, który bardzo mi pomógł i mam nadzieję, że może ci również pomóc.


14

Zobacz ten projekt , pod 5.1.2 [expr.prim.lambda], podrozdział 5:

Typ zamknięcia dla wyrażenia lambda ma publiczny operator wywołania funkcji wbudowanej (13.5.4), którego parametry i typ zwracany są opisane odpowiednio przez klauzulę deklaracji parametru i typ wyrażenia lambda. Ten operator wywołania funkcji jest zadeklarowany jako const (9.3.1) wtedy i tylko wtedy, gdy po klauzuli deklaracji parametru lambdaexpression nie występuje zmienna.

Edytuj w komentarzu litba: Może pomyśleli o przechwytywaniu według wartości, aby zewnętrzne zmiany zmiennych nie były odzwierciedlone w lambda? Referencje działają w obie strony, więc to moje wyjaśnienie. Nie wiem, czy to jest dobre.

Edytuj w komentarzu kizzx2: Najczęściej kiedy lambda ma być używana, jest funktorem algorytmów. Domyślna constness pozwala na używanie jej w stałym środowisku, tak jak constmożna tam zastosować funkcje o normalnych kwalifikacjach, ale nie mogą to robić funkcje constniekwalifikowane. Może po prostu postanowili uczynić to bardziej intuicyjnym dla tych przypadków, którzy wiedzą, co dzieje się w ich umyśle. :)


To standard, ale dlaczego tak to napisali?
kizzx2

@ kizzx2: Moje wyjaśnienie znajduje się bezpośrednio pod tym cytatem. :) Odnosi się to trochę do tego, co litb mówi o żywotności uchwyconych obiektów, ale także idzie nieco dalej.
Xeo

@Xeo: Och tak, tęskniłem za tym: P Jest to również inne dobre wytłumaczenie dobrego wykorzystania przechwytywania według wartości . Ale dlaczego ma to być constdomyślnie? Mam już nową kopię, wydaje się dziwne, że nie mogę jej zmienić - zwłaszcza że nie jest to w zasadzie coś złego - po prostu chcą, żebym dodał mutable.
kizzx2

Sądzę, że podjęto próbę stworzenia nowej składni deklaracji funkcji genowych, wyglądającej podobnie do nazwanej lambda. Miał także rozwiązać inne problemy, domyślnie ustawiając wszystko na stałe. Nigdy nie ukończono, ale pomysły rozwiały się na definicji lambda.
Bo Persson

2
@ kizzx2 - Gdybyśmy mogli zacząć wszystko od nowa, prawdopodobnie mielibyśmy varjako słowo kluczowe, aby pozwolić na zmianę i stały się domyślnymi ustawieniami dla wszystkiego innego. Teraz tego nie robimy, więc musimy z tym żyć. IMO, C ++ 2011 wyszło całkiem nieźle, biorąc pod uwagę wszystko.
Bo Persson

11

Miałem wrażenie, że celem przechwytywania według wartości jest umożliwienie użytkownikowi zmiany ustawienia tymczasowego - w przeciwnym razie prawie zawsze lepiej jest używać przechwytywania przez odniesienie, prawda?

nto nie tymczasowy. n jest członkiem funkcji lambda utworzonej za pomocą wyrażenia lambda. Domyślnym oczekiwaniem jest to, że wywołanie lambda nie modyfikuje jej stanu, dlatego jest stałe, aby zapobiec przypadkowej modyfikacji n.


1
Cały obiekt lambda jest tymczasowy, jego członkowie również mają tymczasowe życie.
Ben Voigt

2
@Ben: IIRC, miałem na myśli kwestię, że kiedy ktoś mówi „tymczasowy”, rozumiem, że oznacza to nienazwany obiekt tymczasowy, którym jest sama lambda, ale jego członkowie nie. A także, że „od wewnątrz” lambda tak naprawdę nie ma znaczenia, czy sama lambda jest tymczasowa. Ponownie czytając pytanie, wydaje się, że OP chciał powiedzieć „n wewnątrz lambda”, kiedy powiedział „tymczasowy”.
Martin Ba

6

Musisz zrozumieć, co oznacza przechwytywanie! przechwytuje, a nie argumentuje! spójrzmy na kilka przykładów kodu:

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

}

Jak widać, mimo że xzmieniono 20na lambda, wciąż zwraca 10 ( xwciąż jest 5w lambdzie). Zmiana xw lambda oznacza zmianę samej lambdy przy każdym wywołaniu (lambda mutuje przy każdym wywołaniu). W celu wymuszenia poprawności standard wprowadził mutablesłowo kluczowe. Określając lambda jako zmienną, mówisz, że każde wywołanie lambda może spowodować zmianę w samej lambda. Zobaczmy inny przykład:

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

}

Powyższy przykład pokazuje, że poprzez zmodulowanie lambdy, zmiana xwewnątrz lambdy „mutuje” lambda przy każdym wywołaniu nową wartością x, która nie ma nic wspólnego z rzeczywistą wartością xfunkcji głównej


4

Obecnie istnieje propozycja zmniejszenia zapotrzebowania na mutabledeklaracje lambda: n3424


Wszelkie informacje o tym, co z tego wynikło? Osobiście uważam, że to zły pomysł, ponieważ nowe „przechwytywanie dowolnych wyrażeń” wygładza większość punktów bólu.
Ben Voigt,

1
@BenVoigt Tak, to wygląda na zmianę.
Miles Rout

3
@BenVoigt Chociaż szczerze mówiąc, spodziewam się, że prawdopodobnie jest wielu programistów C ++, którzy nie wiedzą, że mutablejest to nawet słowo kluczowe w C ++.
Miles Rout

1

Aby rozszerzyć odpowiedź Puppy, funkcje lambda mają być funkcjami czystymi . Oznacza to, że każde wywołanie o unikalnym zestawie wejściowym zawsze zwraca ten sam wynik. Zdefiniujmy dane wejściowe jako zbiór wszystkich argumentów plus wszystkie przechwycone zmienne po wywołaniu lambda.

W funkcjach czystych wyjście zależy wyłącznie od danych wejściowych, a nie od stanu wewnętrznego. Dlatego każda funkcja lambda, jeśli jest czysta, nie musi zmieniać swojego stanu i dlatego jest niezmienna.

Kiedy lambda przechwytuje przez odniesienie, pisanie na przechwyconych zmiennych jest obciążeniem dla koncepcji czystej funkcji, ponieważ wszystko, co powinna zrobić czysta funkcja, to zwrócenie wyniku, chociaż lambda na pewno nie mutuje, ponieważ pisanie dzieje się ze zmiennymi zewnętrznymi. Nawet w tym przypadku poprawne użycie oznacza, że ​​jeśli lambda zostanie wywołana ponownie z tym samym wejściem, wynik będzie za każdym razem taki sam, pomimo tych skutków ubocznych zmiennych typu by-ref. Takie skutki uboczne to tylko sposoby na zwrócenie dodatkowych danych wejściowych (np. Aktualizacja licznika) i można je przeformułować na czystą funkcję, na przykład zwracając krotkę zamiast pojedynczej wartości.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.