Почему лямбда-код C ++ 11 по умолчанию требует ключевое слово mutable для захвата по значению?


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: Не для того, чтобы начинать спор, но, по-моему, понятию «легко учиться» нет места в языке 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
Этот ответ открыл мне глаза. Раньше я думал, что в этом случае лямбда видоизменяет только копию для текущего «запуска».
Жолт Сатмари

4
@ZsoltSzatmari Ваш комментарий открыл мне глаза! : -D Я не понял истинное значение этого ответа, пока я не прочитал ваш комментарий.
Джендас

5
Я не согласен с основной предпосылкой этого ответа. В C ++ нет понятия «функции должны всегда возвращать одно и то же значение» где-либо еще в языке. Как принцип проектирования, я бы согласился, что это хороший способ написать функцию, но я не думаю, что в этом причина стандартного поведения.
Ионокласт Бригам

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
В то время как хорошее объяснение того, как a constили mutableлямбда выглядела бы, если бы реализовывались как эквивалентные пользовательские типы, вопрос (как в заголовке и разработан OP в комментариях) почему const по умолчанию, так что это не отвечает на него.
underscore_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было по умолчанию, по крайней мере, люди были бы вынуждены учитывать правильность const: /
Матье М.

1
@ kizzx2, просматривая лямбда-статьи, мне кажется, они устанавливают значение по умолчанию, constчтобы они могли вызывать его независимо от того, является ли лямбда-объект постоянным. Например, они могли бы передать это функции, принимающей std::function<void()> const&. Чтобы позволить лямбде изменять свои захваченные копии, в первоначальных работах элементы данных замыкания были определены mutableавтоматически внутри. Теперь вам нужно вручную ввести mutableлямбда-выражение. Я не нашел подробного обоснования, хотя.
Йоханнес Шауб - лит


5
На данный момент, мне кажется, что «реальный» ответ / обоснование состоит в том, что «они не смогли обойти детали реализации»: /
kizzx2

32

FWIW, Херб Саттер, известный член комитета по стандартизации 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) и ::operator()реализованный вызов функции . Когда вы захватываете переменную с использованием копирования по значению , компилятор создает новый constатрибут в типе замыкания, поэтому вы не можете изменить его внутри лямбда-выражения, потому что это атрибут «только для чтения», поэтому они Назовите это « закрытием », потому что каким-то образом вы закрываете свое лямбда-выражение, копируя переменные из верхней области в область лямбды.mutableзахваченная сущность станетnon-constАтрибут вашего типа закрытия. Это то, что заставляет изменения, сделанные в изменяемой переменной, захваченной значением, не распространяться в верхнюю область, а оставаться внутри сохраняющей состояние лямбды. Всегда пытайтесь представить себе тип получающегося замыкания лямбда-выражения, которое мне очень помогло, и я надеюсь, что оно может помочь и вам.


14

См. Этот проект в разделе 5.1.2 [expr.prim.lambda], подраздел 5:

Тип закрытия для лямбда-выражения имеет открытый оператор вызова встроенной функции (13.5.4), параметры и тип возвращаемого значения которого описываются с помощью параметра-объявления-выражения и трейлинг-возврата типа лямбда-выражения соответственно. Этот оператор вызова функции объявляется const (9.3.1) тогда и только тогда, когда за параметром-объявлением-параметром lambdaexpression не следует изменяемое.

Редактировать комментарий Литба: Может, они думали о захвате по значению, чтобы внешние изменения переменных не отражались внутри лямбды? Ссылки работают в обе стороны, так что это мое объяснение. Не знаю, хорошо ли это, хотя.

Редактировать комментарий kizzx2: В большинстве случаев использование лямбды является функтором алгоритмов. Ness по умолчанию constпозволяет использовать его в постоянной среде, точно так же, как constтам могут использоваться обычные функции, а не- constквалифицированные - нет. Возможно, они просто подумали, чтобы сделать это более интуитивным для тех случаев, которые знают, что происходит в их уме. :)


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

@ kizzx2: Мое объяснение прямо под этой цитатой. :) Это немного связано с тем, что Литб говорит о времени жизни захваченных объектов, но также идет немного дальше.
Xeo

@Xeo: О да, я пропустил это: P Это еще одно хорошее объяснение правильного использования захвата по стоимости . Но почему это должно быть constпо умолчанию? Я уже получил новую копию, кажется странным не позволять мне ее менять - особенно это не является принципиально неправильным - они просто хотят, чтобы я добавил mutable.
kizzx2

Я полагаю, что была попытка создать новый синтаксис объявления функции genral, похожий на лямбду с именем. Он также должен был решить другие проблемы, сделав все постоянным по умолчанию. Никогда не завершено, но идеи стерлись по лямбда-определению.
Бо Перссон

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

11

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

nэто не временное. n является членом лямбда-функции-объекта, который вы создаете с помощью лямбда-выражения. По умолчанию ожидается, что вызов вашей лямбды не изменяет ее состояние, поэтому он предотвращает случайное изменение n.


1
Весь лямбда-объект является временным, его члены также имеют временное время жизни.
Бен Фойгт

2
@Ben: IIRC, я имел в виду вопрос о том, что когда кто-то говорит «временный», я понимаю, что он означает неназванный временный объект, которым является сама лямбда, но ее члены не являются. А также, что изнутри лямбда не имеет значения, является ли сама лямбда временной. Перечитывая вопрос, кажется, что OP просто хотел сказать «n внутри лямбды», когда сказал «временный».
Мартин Ба

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


Любая информация о том, что из этого вышло? Лично я считаю, что это плохая идея, поскольку новый «захват произвольных выражений» сглаживает большинство болевых точек.
Бен Фойгт

1
@BenVoigt Да, похоже, это изменение ради перемен.
Майлз Рут

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

1

Чтобы расширить ответ Puppy, лямбда-функции должны быть чистыми функциями . Это означает, что каждый вызов с уникальным входным набором всегда возвращает один и тот же результат. Давайте определим входные данные как набор всех аргументов плюс все захваченные переменные при вызове лямбды.

В чистых функциях вывод зависит исключительно от ввода, а не от какого-то внутреннего состояния. Поэтому любая лямбда-функция, если она чистая, не нуждается в изменении своего состояния и поэтому является неизменной.

Когда лямбда захватывает по ссылке, запись в захваченных переменных является проблемой для концепции чистой функции, потому что все, что должна делать чистая функция, - это возвращать вывод, хотя лямбда не обязательно мутирует, потому что запись происходит с внешними переменными. Даже в этом случае правильное использование подразумевает, что если лямбда вызывается с тем же самым вводом снова, вывод будет одинаковым каждый раз, несмотря на эти побочные эффекты для переменных by-ref. Такие побочные эффекты являются всего лишь способами возврата некоторого дополнительного ввода (например, обновления счетчика) и могут быть преобразованы в чистую функцию, например, возвращая кортеж вместо одного значения.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.