Почему С++ 0x lambda требует по умолчанию "изменяемого" ключевого слова для захвата по значению?

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

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

Ответы

Ответ 1

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

Ответ 2

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

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

Итак, вы можете думать о lambdas как о создании класса с operator(), который по умолчанию имеет значение const, если вы не скажете, что он изменен.

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

Ответ 3

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

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

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

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

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

Ответ 4

FWIW, Herb Sutter, известный член комитета по стандартизации С++, дает другой ответ на этот вопрос в Lambda Correctness and Usability Issues:

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

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’

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

В его статье говорится, почему это должно быть изменено в С++ 14. Это короткий, хорошо написанный, заслуживающий внимания, если вы хотите знать, "что на [членах комитета]" в отношении этой конкретной функции.

Ответ 5

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

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

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

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

Ответ 6

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

Ответ 7

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

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

Ответ 8

В настоящее время предлагается смягчить необходимость mutable в объявлениях лямбда: n3424