Чому лямбда C ++ 11 за замовчуванням вимагає "змінне" ключове слово для отримання даних за значенням?


256

Короткий приклад:

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

Питання: Для чого нам потрібне mutableключове слово? Він зовсім відрізняється від традиційного переходу параметра до названих функцій. Що за обґрунтування?

У мене склалося враження, що вся суть захоплення за значенням полягає в тому, щоб дозволити користувачеві змінити тимчасову - інакше мені майже завжди краще використовувати захоплення за посиланням, чи не так?

Будь-які просвіти?

(Я, до речі, використовую MSVC2010. AFAIK це має бути стандартним)


101
Гарне питання; хоча я радий щось нарешті constза замовчуванням!
xtofl

3
Не відповідь, але я вважаю, що це розумна річ: якщо ви берете щось за значенням, ви не повинні змінювати це лише для того, щоб зберегти 1 копію на локальну змінну. Принаймні, ви не помилитесь, змінивши n, замінивши = на &.
stefaanv

8
@xtofl: Не впевнений, що це добре, коли все інше не constза замовчуванням.
kizzx2

8
@ Tamás Szelei: Не наводити аргументи, але поняття IMHO "легко засвоїти" не має місця в мові C ++, особливо в сучасні дні. У будь-якому разі: P
kizzx2

3
"вся точка захоплення за вартістю полягає в тому, щоб дозволити користувачу змінити тимчасову" - Ні, вся суть у тому, що лямбда може залишатися дійсною протягом життя будь-яких захоплених змінних. Якби лямбди C ++ мали лише захоплення за рефреш, вони були б непридатними для занадто багато сценаріїв.
Себастьян Редл

Відповіді:


230

Це потрібно, mutableтому що за замовчуванням функціональний об'єкт повинен створювати однаковий результат кожного разу, коли він викликається. Це різниця між об'єктно-орієнтованою функцією та функцією, що ефективно використовує глобальну змінну.


7
Це хороший момент. Я цілком погоджуюся. Однак у C ++ 0x я не зовсім розумію, як за замовчуванням допомагає виконувати вищесказане. Подумайте, я перебуваю на приймальному кінці лямбда, наприклад, я є void f(const std::function<int(int)> g). Як я гарантую, що gнасправді референтно прозорий ? gПостачальник, можливо, mutableвсе-таки використовував . Тож я не знаю. З іншого боку, якщо за замовчуванням відмінно const, і люди повинні додати constзамість mutableдо об'єктів функції, компілятор може реально забезпечити дотримання const std::function<int(int)>частини , і тепер fможна припустити , що gце const, немає?
kizzx2

8
@ kizzx2: У C ++ нічого не застосовується , лише пропонується. Як звичайно, якщо ви робите щось дурне (документально підтверджена вимога щодо прозорості референції, а потім проходите нереференційно-прозору функцію), ви отримуєте все, що вам доходить.
Щеня

6
Ця відповідь відкрила мені очі. Раніше я думав, що в цьому випадку лямбда лише мутує копію для поточного "запуску".
Zsolt Szatmari

4
@ZsoltSzatmari Ваш коментар відкрив мені очі! : -Я не отримав справжнього значення цієї відповіді, поки я не прочитав ваш коментар.
Jendas

5
Я не погоджуюся з основною передумовою цієї відповіді. C ++ не має поняття "функції завжди повинні повертати те саме значення" в будь-якому іншому місці мови. В принципі конструкції, я згоден , що це хороший спосіб , щоб написати функцію, але я не думаю , що він тримає воду , як на причини для стандартного поведінки.
Ionoclast Brigham

103

Ваш код майже еквівалентний цьому:

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

Таким чином, ви можете подумати про лямбда як про генерування класу з оператором (), який за замовчуванням const, якщо ви не скажете, що він є змінним.

Ви також можете вважати всі змінні, захоплені всередині [] (явно або неявно) як члени цього класу: копії об'єктів для [=] або посилання на об'єкти для [&]. Вони ініціалізуються, коли ви оголошуєте свою лямбда як би прихований конструктор.


5
Хоча приємне пояснення того, як виглядатиме амбда constчи mutableлямбда, якби реалізовуватись як еквівалентні визначені користувачем типи, питання (як у заголовку та розроблено ОП у коментарях), чому const це за замовчуванням, тому це не відповідає на нього.
підкреслюю_d

36

У мене склалося враження, що вся суть захоплення за значенням полягає в тому, щоб дозволити користувачеві змінити тимчасову - інакше мені майже завжди краще використовувати захоплення за посиланням, чи не так?

Питання в тому, це "майже"? Частим випадком використання є повернення або передача лямбдів:

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

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

Я думаю, що mutableце не «майже». Я вважаю, що "захоплення за значенням" на зразок "дозволити мені використовувати його значення після вмирання захопленого об'єкта", а не "дозволити мені змінити його копію". Але, можливо, це можна стверджувати.


2
Хороший приклад. Це дуже вагомий випадок використання для захоплення за вартістю. Але чому це за замовчуванням const? Якої мети вона досягає? mutableздається , недоречні тут, коли constце НЕ за замовчуванням в «майже» (: P) все інше мови.
kizzx2

8
@ kizzx2: Я б хотів, щоб це constбуло за замовчуванням, принаймні люди були б змушені враховувати коректність: /
Matthieu M.

1
@ kizzx2, дивлячись на документи лямбда, мені здається, вони роблять це за замовчуванням, щоб constвони могли назвати це, чи є об'єкт лямбда const чи ні. Наприклад, вони могли передати його функції, яка приймає a std::function<void()> const&. Щоб дозволити лямбда змінювати свої захоплені копії, у початкових документах члени закритих даних були визначені mutableвнутрішньо автоматично. Тепер ви повинні вручну ввести mutableлямбда-вираз. Однак я не знайшов детального обґрунтування.
Йоханнес Шауб - ліб


5
На цей момент, як мені здається, "справжньою" відповіддю / обгрунтуванням здається, що "вони не змогли обійти деталі щодо впровадження": /
kizzx2

32

FWIW, Herb Sutter, відомий член комітету зі стандартизації C ++, надає іншу відповідь на це питання в питаннях коректності та зручності використання 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’

Здається, ця функція була додана з побоювання, що користувач може не усвідомлювати, що він отримав копію, і, особливо, оскільки, оскільки лямбда-файли копіюються, він може змінити іншу лямбда-копію.

Його праця розповідає про те, чому це слід змінити в C ++ 14. Це коротке, добре написане, яке варто прочитати, якщо ви хочете дізнатися про те, що у голові [члена комітету] щодо цієї особливості.


16

Вам потрібно подумати, який тип закриття вашої функції лямбда. Кожного разу, коли ви заявляєте вираз Lambda, компілятор створює тип закриття, який є не менш ніж оголошення без назви класу з атрибутами ( середовище, де вираз Lambda, де оголошено) та ::operator()реалізований виклик функції . Коли ви захоплюєте змінну за допомогою копіювання за значенням , компілятор створить новий constатрибут типу закриття, тому ви не можете змінити його всередині виразу Lambda, оскільки це атрибут "тільки для читання", тому вони називаємо це " закриттям ", тому що якимось чином ви закриваєте свій вираз Lambda, копіюючи змінні з верхньої області в область Lambda.mutable, захоплена сутність стане non-constатрибутом вашого типу закриття. Саме це призводить до того, що зміни, внесені у змінну змінну, захоплену за значенням, не поширюються на верхній обсяг, а зберігають всередині стані Лямбда. Завжди намагайтеся уявити отриманий тип закриття вашого лямбдаського виразу, який мені дуже допоміг, і я сподіваюся, що він теж може вам допомогти.


14

Дивіться цей проект під п. 5.1.2 [expr.prim.lambda], підпункт 5:

Тип закриття лямбда-виразу має оператор виклику функцій загального вбудованого функціонування (13.5.4), параметри та тип повернення описуються відповідно параметром декларування-лямбда-виразу та заднім типом повернення. Оператор виклику цієї функції оголошується const (9.3.1) тоді і лише тоді, коли параметр-декларація параметра лямбдаекспресії не супроводжується зміною.

Редагувати в коментарі litb: Можливо, вони подумали про захоплення за вартістю, щоб зовнішні зміни змінних не відображалися всередині лямбда? Посилання працюють обома способами, тому це моє пояснення. Не знаю, чи добре це все-таки.

Редагувати в коментарі kizzx2: Найчастіше, коли лямбда використовується, є функцією для алгоритмів. constНісс за замовчуванням дозволяє використовувати його в постійному середовищі, як звичайнеconstНісс там можна використовувати кваліфіковані функції, але constнекваліфіковані не можуть. Можливо, вони просто думали зробити це більш інтуїтивним для тих випадків, які знають, що відбувається в їхній свідомості. :)


Це стандарт, але чому вони написали це так?
kizzx2

@ kizzx2: Моє пояснення прямо під цією цитатою. :) Це трохи стосується того, що лампа говорить про час життя захоплених предметів, але також йде трохи далі.
Xeo

@Xeo: О так, я пропустив це: P Це ще одне хороше пояснення для хорошого використання функції захоплення за значенням . Але чому це повинно бути constза замовчуванням? Я вже отримав нову копію, здається дивним, щоб не дозволити мені її змінити - особливо це не щось принципово не так - вони просто хочуть, щоб я додав mutable.
kizzx2

Я вважаю, що була спроба створити новий синтаксис оголошення загальної функції, схожий на названий лямбда. Також слід було виправити інші проблеми, зробивши все const за замовчуванням. Ніколи не завершувались, але ідеї втиралися в визначення лямбда.
Бо Персон

2
@ kizzx2 - Якби ми могли почати все заново, ми, мабуть, мали varб ключове слово, щоб дозволити зміни та постійне значення за замовчуванням для всього іншого. Зараз ми цього не робимо, тому нам доведеться з цим жити. IMO, C ++ 2011 вийшов досить добре, враховуючи все.
Бо Персон

11

У мене склалося враження, що вся суть захоплення за значенням полягає в тому, щоб дозволити користувачеві змінити тимчасову - інакше мені майже завжди краще використовувати захоплення за посиланням, чи не так?

nце НЕ тимчасове. n - член об’єкта лямбда-функції, який ви створюєте за допомогою виразу лямбда. За замовчуванням очікується, що виклик вашої лямбда не змінює її стан, тому просто запобігти випадковому зміні n.


1
Весь об’єкт лямбда є тимчасовим, його члени також мають тимчасовий термін експлуатації.
Бен Войгт

2
@Ben: IIRC, я мав на увазі питання про те, що коли хтось каже "тимчасовий", я розумію, що це означає неназваний тимчасовий об'єкт, яким є сама лямбда, але це не члени. А також те, що зсередини лямбда не має значення, тим самим лямбда чи тимчасова. Перечитавши питання, хоча, здається, що ОП просто мав на увазі сказати "російське всередині лямбда", коли він сказав "тимчасовий".
Мартін Ба

6

Ви повинні зрозуміти, що означає захоплення! це захоплення не аргумент передачі! давайте розглянемо кілька зразків коду:

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

}

Як ви бачите, навіть якщо xзмінено на 20лямбда, все ще повертається 10 ( xвсе ще знаходиться 5всередині лямбда). Зміна xвсередині лямбда означає зміну самої лямбда при кожному дзвінку (лямбда мутує при кожному дзвінку). Для забезпечення правильності стандарт ввів mutableключове слово. Вказуючи лямбду як змінну, ви говорите, що кожен виклик лямбда може викликати зміну самого лямбда. Подивіться інший приклад:

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

}

Наведений вище приклад показує, що, роблячи лямбда-змінним, зміна xвсередині лямбда "мутує" лямбда при кожному виклику з новим значенням, xяке не має нічого спільного з фактичним значенням xосновної функції


4

Зараз є пропозиція полегшити потребу mutableв лямбда-деклараціях: n3424


Будь-яка інформація про те, що з цього вийшло? Я особисто вважаю, що це погана ідея, оскільки новий "захоплення довільних виразів" згладжує більшість больових точок.
Ben Voigt

1
@BenVoigt Так, це здається зміною заради змін.
Майлз Рут

3
@BenVoigt Хоча, справедливо кажучи, я думаю, що, мабуть, існує багато розробників C ++, які не знають, що mutableце навіть ключове слово в C ++.
Майлз Рут

1

Щоб розширити відповідь Щеня, лямбда-функції призначені як чисті функції . Це означає, що кожен виклик, отриманий унікальним набором вводу, завжди повертає один і той же вихід. Давайте визначимо вхід як набір усіх аргументів плюс усі захоплені змінні, коли лямбда викликається.

У чистих функціях вихід залежить виключно від введення, а не від якогось внутрішнього стану. Тому будь-яка лямбда-функція, якщо вона чиста, не потребує зміни свого стану і тому є незмінна.

Коли лямбда захоплює посиланням, запис на захоплених змінних є напругою щодо концепції чистої функції, тому що вся чиста функція повинна робити, це повернути вихід, хоча лямбда не мусить, безумовно, мутувати, оскільки запис відбувається із зовнішніми змінними. Навіть у цьому випадку правильне використання означає, що якщо лямбда викликається знову з одним і тим же входом, вихід буде кожного разу однаковим, незважаючи на ці побічні ефекти на змінних, що перераховуються. Такі побічні ефекти є лише способами повернути деякий додатковий вклад (наприклад, оновити лічильник) і можуть бути переформульовані в чисту функцію, наприклад, повернення кортежу замість одного значення.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.