Почему лямбда-код C++11 по умолчанию требует ключевое слово mutable для захвата по значению?
Краткий пример:
#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 это должно быть стандартным)
14 ответов
Это требует mutable
потому что по умолчанию объект функции должен выдавать один и тот же результат при каждом вызове. В этом разница между объектно-ориентированной функцией и функцией, эффективно использующей глобальную переменную.
Ваш код почти эквивалентен этому:
#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, если только вы не скажете, что он изменчив.
Вы также можете рассматривать все переменные, захваченные внутри [] (явным или неявным образом), как члены этого класса: копии объектов для [=] или ссылки на объекты для [&]. Они инициализируются, когда вы объявляете лямбду, как если бы был скрытый конструктор.
Вы должны понять, что означает захват! это захват не передачи аргументов! давайте посмотрим на некоторые примеры кода:
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
в основной функции
У меня сложилось впечатление, что весь смысл захвата по значению состоит в том, чтобы позволить пользователю изменить временное - иначе мне почти всегда лучше использовать захват по ссылке, не так ли?
Вопрос в том, "почти" ли это? Частым случаем использования, как представляется, является возврат или передача лямбд:
void registerCallback(std::function<void()> f) { /* ... */ }
void doSomething() {
std::string name = receiveName();
registerCallback([name]{ /* do something with name */ });
}
я думаю что mutable
не случай "почти". Я считаю, что "захват по значению", как "разрешить мне использовать его значение после смерти захваченного объекта", а не "разрешить мне изменить его копию". Но, возможно, с этим можно поспорить.
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. Это краткое, хорошо написанное, заслуживающее прочтения, если вы хотите знать, "что умы [члена комитета]" касаются этой конкретной функции.
Вам нужно подумать, каков тип закрытия вашей лямбда-функции. Каждый раз, когда вы объявляете лямбда-выражение, компилятор создает тип замыкания, который является не чем иным, как объявлением класса без имени с атрибутами (среда, в которой было объявлено выражение Lambda) и вызовом функции. ::operator()
реализованы. Когда вы перехватываете переменную с использованием копирования по значению, компилятор создаст новый const
атрибут в типе замыкания, поэтому вы не можете изменить его внутри лямбда-выражения, потому что это атрибут "только для чтения", поэтому они называют его "замыканием", потому что в некотором смысле вы закрываете свое лямбда-выражение копируя переменные из верхней области в область лямбды. Когда вы используете ключевое слово mutable
захваченная сущность станет non-const
Атрибут вашего типа закрытия. Это то, что заставляет изменения, сделанные в изменяемой переменной, захваченной значением, не распространяться в верхнюю область видимости, а оставаться внутри сохраняющей состояние лямбды. Всегда старайтесь вообразить получающийся тип замыкания вашего лямбда-выражения, который мне очень помог, и я надеюсь, что он вам тоже поможет.
См. Этот проект в разделе 5.1.2 [expr.prim.lambda], подраздел 5:
Тип замыкания для лямбда-выражения имеет открытый оператор вызова встроенной функции (13.5.4), параметры и тип возвращаемого значения которого описываются с помощью параметра-объявления-выражения лямбда-выражения и типа трейлинг-возврата соответственно. Этот оператор вызова функции объявляется const (9.3.1) тогда и только тогда, когда за параметром-объявлением-параметром lambdaexpression не следует изменяемое.
Отредактируйте комментарий Литба: Может, они подумали о захвате по значению, чтобы внешние изменения переменных не отражались внутри лямбды? Ссылки работают в обе стороны, так что это мое объяснение. Не знаю, хорошо ли это, хотя.
Редактировать комментарий kizzx2: В большинстве случаев использование лямбды является функтором алгоритмов. По умолчанию const
Ness позволяет использовать его в постоянной среде, как обычно const
можно использовать функции, но неconst
Квалифицированные не могут. Возможно, они просто подумали, чтобы сделать его более интуитивным для тех случаев, которые знают, что происходит в их уме.:)
У меня сложилось впечатление, что весь смысл захвата по значению состоит в том, чтобы позволить пользователю изменить временное - иначе мне почти всегда лучше использовать захват по ссылке, не так ли?
n
is not a temporary. n is a member of the lambda-function-object that you create with the lambda expression. The default expectation is that calling your lambda does not modify its state, therefore it is const to prevent you from accidentally modifying n
,
Чтобы расширить ответ Puppy, лямбда-функции должны быть чистыми функциями. Это означает, что каждый вызов с уникальным входным набором всегда возвращает один и тот же результат. Давайте определим входные данные как набор всех аргументов плюс все захваченные переменные при вызове лямбды.
В чистых функциях вывод зависит исключительно от ввода, а не от какого-то внутреннего состояния. Поэтому любая лямбда-функция, если она чистая, не нуждается в изменении своего состояния и поэтому является неизменной.
Когда лямбда захватывает по ссылке, запись в захваченных переменных является проблемой для концепции чистой функции, потому что все, что должна делать чистая функция, - это возвращать вывод, хотя лямбда не обязательно мутирует, потому что запись происходит с внешними переменными. Даже в этом случае правильное использование подразумевает, что если лямбда вызывается с тем же самым вводом снова, вывод будет одинаковым каждый раз, несмотря на эти побочные эффекты на переменные by-ref. Такие побочные эффекты являются всего лишь способами возврата некоторого дополнительного ввода (например, обновления счетчика) и могут быть преобразованы в чистую функцию, например, возвращая кортеж вместо одного значения.
Мне также было интересно об этом, и самое простое объяснение, почему требуется явное, в этом примере:
int main()
{
int x {1};
auto lbd = [=]() mutable { return x += 5; };
printf("call1:%d\n", lbd());
printf("call2:%d\n", lbd());
return 0;
}
Выход:
call1:6
call2:11
По словам:
Вы можете видеть, что
x
значение отличается при втором вызове (1 для вызова1 и 6 для вызова2).
- Лямбда-объект хранит захваченную переменную по значению (имеет собственную копию) на случай
[=]
. - Лямбду можно вызывать несколько раз.
И в общем случае мы должны иметь одно и то же значение захваченной переменной, чтобы иметь такое же предсказуемое поведение лямбды на основе известного захваченного значения, не обновленного во время работы лямбды. Вот почему предполагается поведение по умолчанию
const
(для прогнозирования изменений членов лямбда-объекта), и когда пользователь осознает последствия, он берет эту ответственность на себя с
mutable
.
То же самое с захватом по значению. Для моего примера:
auto lbd = [x]() mutable { return x += 5; };
Вы можете увидеть разницу, если проверите 3 разных варианта использования лямбда:
- Захват аргумента по значению
- Захват аргумента по значению с ключевым словом mutable
- Захват аргумента по ссылке
случай 1: когда вы фиксируете аргумент по значению, происходит несколько вещей:
- Вам не разрешено изменять аргумент внутри лямбда
- Значение аргумента остается неизменным всякий раз, когда вызывается лямбда, независимо от того, каким будет значение аргумента во время вызова лямбды.
так например:
{
int x = 100;
auto lambda1 = [x](){
// x += 2; // compile time error. not allowed
// to modify an argument that is captured by value
return x * 2;
};
cout << lambda1() << endl; // 100 * 2 = 200
cout << "x: " << x << endl; // 100
x = 300;
cout << lambda1() << endl; // in the lambda, x remain 100. 100 * 2 = 200
cout << "x: " << x << endl; // 300
}
Output:
200
x: 100
200
x: 300
случай 2: здесь, когда вы захватываете аргумент по значению и используете ключевое слово 'mutable', аналогично первому случаю, вы создаете "копию" этого аргумента. Эта «копия» находится в «мире» лямбда-выражения, но теперь вы можете фактически изменить аргумент в лямбда-мире, поэтому его значение будет изменено и сохранено, и на него можно будет ссылаться в будущих вызовах этого лямбда. Опять же, внешняя «жизнь» аргумента может быть совершенно другой (с точки зрения ценности):
{
int x = 100;
auto lambda2 = [x]() mutable {
x += 2; // when capture by value, modify the argument is
// allowed when mutable is used.
return x;
};
cout << lambda2() << endl; // 100 + 2 = 102
cout << "x: " << x << endl; // in the outside world - x remains 100
x = 200;
cout << lambda2() << endl; // 104, as the 102 is saved in the lambda world.
cout << "x: " << x << endl; // 200
}
Output:
102
x: 100
104
x: 200
случай 3: Это самый простой случай, так как не более 2 жизней х. Теперь есть только одно значение x, и оно разделяется между внешним миром и миром лямбда.
{
int x = 100;
auto lambda3 = [&x]() mutable {
x += 10; // modify the argument, is allowed when mutable is used.
return x;
};
cout << lambda3() << endl; // 110
cout << "x: " << x << endl; // 110
x = 400;
cout << lambda3() << endl; // 410.
cout << "x: " << x << endl; // 410
}
Output:
110
x: 110
410
x: 410
В настоящее время существует предложение смягчить необходимость mutable
в лямбда-объявлениях: n3424
Это действительно смешивает два вопроса в один, и их разделение облегчает предоставление ответов:
- Почему лямбда-захват по значению, изменяющему захваченную переменную, имеет неверный формат?
- Почему значения по умолчанию фиксируются лямбда-выражениями?
На вопрос №1 есть простой фактический ответ: значения, полученные с помощью лямбда-выражений, используются по умолчанию. В этих случаях:
int n = 0;
[=](){n = 10;}();
[n](){n = 10;}();
[val = n](){val = 10;}();
Вопрос №2 — это вопрос о языковом дизайне, и ответ на него может быть сложным или неоднозначным. Многие возможные ответы уже были даны в других ответах здесь, но многие из них не отвечают на вопрос № 1, поэтому я думаю, что они бесполезны для посетителей, которых интересует только то, как работает лямбда-захват, а не обсуждение принципов и рассуждений языкового дизайна. .
Когда вы используете лямбду в качестве параметра функции, ответ выделяется!