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

Краткий пример:

#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, это должно быть стандартным)

Ответы [ 10 ]

213 голосов
/ 31 марта 2011

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

97 голосов
/ 10 сентября 2012

Ваш код почти эквивалентен этому:

#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, если только вы не скажете, что он изменчив.1005 * Вы также можете рассматривать все переменные, захваченные внутри [] (явно или неявно), как члены этого класса: копии объектов для [=] или ссылки на объекты для [&].Они инициализируются, когда вы объявляете лямбду, как если бы был скрытый конструктор.

35 голосов
/ 31 марта 2011

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

Вопрос в том, "почти" ли?Частым случаем использования, как представляется, является возврат или передача лямбды:

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

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

Я думаю, что mutable - это не случай "почти".Я считаю, что «захват по значению», как «позволяет мне использовать его значение после смерти захваченного объекта», а не «позволяет мне изменить его копию».Но, возможно, с этим можно поспорить.

27 голосов
/ 20 января 2014

FWIW, Херб Саттер, известный член комитета по стандартизации C ++, дает другой ответ на этот вопрос в Лямбда-вопросы правильности и удобства использования :

Учтите этоПример соломенного человека, где программист захватывает локальную переменную по значению и пытается изменить захваченное значение (которое является переменной-членом лямбда-объекта):

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.Это краткое, хорошо написанное, заслуживающее прочтения, если вы хотите знать «что у [члена комитета] умы» относительно этой конкретной функции.

14 голосов
/ 31 марта 2011

См. этот черновик , под 5.1.2 [expr.prim.lambda], подпункт 5:

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

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

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

12 голосов
/ 31 марта 2011

Вам нужно подумать, что такое тип закрытия вашей лямбда-функции.Каждый раз, когда вы объявляете лямбда-выражение, компилятор создает тип замыкания, который является не чем иным, как объявлением класса без имени с атрибутами ( environment , где было объявлено выражение Lambda) и реализован вызов функции ::operator().Когда вы захватываете переменную, используя copy-by-value , компилятор создает новый атрибут const в типе замыкания, поэтому вы не можете изменить его внутри лямбда-выражения, потому что это «чтение»-только «атрибут», поэтому они называют его « замыканием », потому что в некотором смысле вы закрываете свое лямбда-выражение, копируя переменные из верхней области в область лямбды.Когда вы используете ключевое слово mutable, захваченная сущность станет атрибутом non-const вашего типа закрытия.Это то, что заставляет изменения, сделанные в изменяемой переменной, захваченной значением, не распространяться в верхнюю область, а оставаться внутри сохраняющей состояние лямбды.Всегда пытайтесь представить себе тип получающегося в результате замыкания вашего лямбда-выражения, который мне очень помог, и я надеюсь, что он вам тоже поможет.

9 голосов
/ 01 апреля 2011

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

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

.
4 голосов
/ 03 ноября 2012

В настоящее время существует предложение уменьшить потребность в mutable в лямбда-декларациях: n3424

3 голосов
/ 22 февраля 2019

Вы должны понять, что означает захват!это захват не передачи аргументов!давайте рассмотрим несколько примеров кода:

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 в основной функции

0 голосов
/ 02 июня 2018

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...