Lambda capture copying requires mutable declaration

Pages: 12
I'm not really understanding why the foo() lambda requires being declared as mutable in the following bit of code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>

int main()
{
   int x { 1 };
   int y { 1 };

   std::cout << x << ' ' << y << '\n';

   auto foo = [x, y] () mutable
   {
      ++x;
      ++y;

      std::cout << x << ' ' << y << '\n';
   };

   foo();

   std::cout << x << ' ' << y << '\n';
}

The lambda behavior is certainly not normal IMO, copied non-const parameters in a regular function are modifiable within the scope of the function without the function being declared as mutable. Why are lambdas treated differently?

Is it as simple as "because the standard says so?"
Last edited on
The standard does say that mutable must be specified to write to captures or to call their non-const functions, so the immediate reason why lambdas as treated differently is indeed because the standard says so. Now, why the standard requires this behavior, I don't know. Might be to by default prevent confusing behavior such as the function writing to objects and the change not being reflected outside. it seems like a strange reason, but I can't think of anything else.
A lambda has an operator() member function just like any function-like class. By default this operator() is const, but you can remove it by making the lambda mutable. The const is problematic because lambda captures correspond to class member data, which can't be changed within a const member function.

The question here is why const was chosen as the default, unlike everywhere else in the language.

At one point during the language design process, lambdas didn't have any meaningful notion of const-ness. So n2651 specified a few alternatives to integrate lambda expressions with const:
https://wg21.link/n2651

The favored alternative was to make the operator() non-const by default and let users opt-in to const by writing const where mutable goes today:
auto foo = [x] () { ++x; } // ok
or
auto bar = [x] () const { ++x; } // error

Slightly later, n2658 "revised" n2651 by choosing to opt-out of const, settling on the current behavior. This isn't even one of the alternatives listed in n2651. No rationale is given.
https://wg21.link/n2658

Maybe I am missing a crucial detail but this seems to be a bad decision.
Last edited on
Thanks for the replies guys.

Committees do weird stuff for strange and largely unknown and unknowable reasons to us peons on the outside. As long as I remember the "how-to" I'm "OK."

Hopefully it doesn't radically change the default to a different way of doing things in a later standard.
Committees do weird stuff for strange and largely unknown and unknowable reasons to us peons on the outside.

For me an understanding of the motivation & design considerations behind a feature is the fastest way to understand where and when the feature is useful. But n2651 offers none of that information. Who is responsible for disseminating this info if not the authors of the design document?
Last edited on
Here is one reason why lambda is const by default:

The current object (*this) can be implicitly captured if either capture default is present. If (*this) is implicitly captured, it is always captured by reference


where,
The only capture defaults are & and =


This means if the lambda would be non const by default then accidental modifications of implicitly captured *this would be possible, but the implicitly captured object may or may not be const qualified, therefore const by default is correct to assume.

However this behavior is deprecated in C++20, now implicit capture of this applies only to default
&


Another reason why lambda is const by default is what @mbozzi started to explain but didn't say...
Lambdas are interpreted as anonymous classes by the compiler, they are referred to as "ClosureType" class, which just like "normal" classes may have special member functions that are always generated by the compiler..

those autogenerated functions perform a shallow copy\move semantics of captured objects, therefore if your captured object contains dynamically allocated objects it would not be properly copied or moved by those special member functions.

Assuming captured object is const by default is then again correct assumption.
This is in detail explained under "ClosureType::Captures" section of the cppreference.

https://en.cppreference.com/w/cpp/language/lambda
Last edited on
This means if the lambda would be non const by default then accidental modifications of implicitly captured *this would be possible, but the implicitly captured object may or may not be const qualified, therefore const by default is correct to assume.
That can't be right.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>

struct A{
    void f(){
        std::cout << "non const\n";
    }
    void f() const{
        std::cout << "const\n";
    }
    void do_it(){
        std::cout << "do_it()\n";
        [this](){
            this->f();
        }();
        [*this](){
            this->f();
        }();
        [=](){
            this->f();
        }();
        [&](){
            this->f();
        }();
    }
    void do_it2() const{
        std::cout << "do_it2()\n";
        [this](){
            this->f();
        }();
        [*this](){
            this->f();
        }();
        [=](){
            this->f();
        }();
        [&](){
            this->f();
        }();
    }
};

int main(){
    A a;
    a.do_it();
    a.do_it2();
}

Output:

do_it()
non const
const
non const
non const
do_it2()
const
const
const
const

It seems the compiler is perfectly fine calling non-const member functions of this, except when this is explicitly copied into the lambda.

those autogenerated functions perform a shallow copy\move semantics of captured objects, therefore if your captured object contains dynamically allocated objects it would not be properly copied or moved by those special member functions.
That's silly. It's the programmer's job to understand the copy semantics of the objects he's using. If he doesn't then even forbidding non-const calls may not be enough to prevent foot shootings.
helios,
I badly expressed myself, by "accidental modifications of implicitly captured *this" I meant that the coder may modify this for real (implicit capture by reference) while he did not intend to do so, not that the const qualifier would cast away.

Compiler works as it should but consider following modified example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>

struct A
{
	void do_it1()
	{
		std::cout << "do_it()\n";

		[=]()
		{
			this->x = 1;
		}();
	}

	void do_it2()
	{
		std::cout << "do_it2()\n";

		[&]()
		{
			this->x = 2;
		}();
	}

	int x = 0;
};

int main()
{
	A a;
	std::cout << a.x << std::endl;

	a.do_it1();
	std::cout << a.x << std::endl;

	a.do_it2();
	std::cout << a.x << std::endl;
}


Here in this code sample in both cases x is modified for real!

But a casual coder may believe that do_it1() will not modify this for real because it was implicitly captured by copy rather than by reference which is wrong regardless of [=] capture default.

This behavior has been fixed in C++20, where do_it1() will not modify the real this

Now, what does this have to do with lambda being const by default?
I'm not really sure at this point but likely because of consistency with capture by reference version, that is to prevent "self shooting" that was possible with implicit capture of this and forcing the coder to explicitly type mutable.

That's silly. It's the programmer's job to understand the copy semantics of the objects he's using. If he doesn't then even forbidding non-const calls may not be enough to prevent foot shootings.

That's exactly why I think why default is const, if programmer explicitly writes mutable then he declares he is probably aware of consequences.
But a casual coder

That's me! To a T! Self-taught for decades, and still a beginner for most things.

I muck around with C++, only occasionally daring to go out to the fringes of what can be done away from a nice safe center of "what's been done before."

I like what can be done with lambdas, they do simplify code so it can be less bulky of a read.
Last edited on
Also with lambdas you can define them within a function - so you sort of can get 'function within a function' which isn't supported by c/C++

For the 'last word' on lambdas, see https://leanpub.com/cpplambda
Last edited on
Heh, seeplus, you link to the very eBook I am currently slogging my way through yet again, trying to cram the history and usage of lambdas into my grey matter. This isn't my first ride with this rodeo bronco.

I recommend the book without question to anyone who wants to understand more about lambdas than they already know. Lots of examples.

Just finished the 2nd chapter, freshly minted lambdas in C++11. A bit more illumination than before, still more fog than not.

My code snippet above is a SSCCE from one of the early code examples in the chapter.

Kinda hard to muck around with C++11 lambdas using Visual Studio 2019 since the furthest back the compiler can be set back is C++14.

But now with the lastest 2019 version I should be able to actually get the code in the C++20 chapter to work as expected! *Cheer!*
Last edited on
Hi Furry Guy

This link could help or not. :)

http://websites.umich.edu/~eecs381/handouts/Lambda.pdf
Last edited on
Yes. Good book. The Author also did one on C++17. Just as good. He also has an active C++ blog https://www.cppstories.com/

Simply the answer to the question is that value captures are by const. As in a class, if the class is defined as const but you still want a member to be able to be updated (++x here) then you need to define them as mutable.



Have his C++17 eBook as well, C++17 in Detail

Leanpub is a nice resource. I got 3 eBooks there written by Nicolai M. Josuttis:

1. C++ Move Semantics - The Complete Guide
2. C++17 - The Complete Guide
3. C++20 - The Complete Guide
Last edited on
Last edited on
I have those books on my to-buy list. Sadly even as eBooks the cost for them even individually would take a considerable bite out of my very small, very limited fixed income. I will get them as time and cash flow permits.
@seeplus,

That second edition about templates is new one (I'm getting it), but if you want to go one step further and eat templates for breakfast then read also:

"Template Metaprogramming in C++" by Keith Schwarz
Gotta a link for that? PLEASE! *sad puppy dog eyes*

I know, I know, I could find one myself, but I wanna beg like a dog wanting some bacon....
Last edited on
This is link to the official site of the author:
https://www.keithschwarz.com/talks/slides/tmp-cs242.pdf

root page:
https://www.keithschwarz.com/

EDIT:
Also search for PDF:
"What Every Programmer Should Know About Memory"

Is more useful for assembly coder, but it doesn't hurt to know more, followed by:
"Pushing the limits of windows memory" MSDN article series!
Last edited on
Thank you, thank you, thank you! :)

Oooh, a free "book"! I like that price!
Pages: 12