9

Why are captured-by-value values const, but captured-by-reference objects not:

int a;

auto compile_error = [=]()
{
  a = 1;
}
auto compiles_ok = [&]()
{
  a = 1;
}

To me this seem illogical but it seem to be the standard? Especially as the unwanted modification of a captured value may be an annoying bug, but chances are high that the consequences are limited to lambda scope, whereas unwanted modification of objects captured by reference will often lead to more serious effects.

So why not capture by const reference per default? Or at least support [const &] and [&]? What are the reasons for this design?

As workaround you are probably supposed to use std::cref wrapped const references captured by value?

valoh
  • 393
  • 1
  • 5
  • 6
  • Much of the point in capturing a reference is changing it. There is *no* reason to change a value - it is meaningless. – Elazar May 26 '13 at 22:12
  • 3
    References are immutable and can never be changed after being bound. So in a sense lambda expressions are consistent with the rest of the language here. You might want to learn the meaning of `mutable` for lambda expressions. – Luc Danton May 26 '13 at 22:16
  • @Elazar: Values as temporary may be changed, but still as you suggest it is no issue to change a value, whereas changing a referenced object really impacts other scopes which may be unintended (bugs). – valoh May 26 '13 at 22:26
  • @Luc Danton: You want a const reference so that it is ensured that the referenced objects isn't changed unintended. And if you really want to change objects from outer scopes you should explicitly write it (const & vs & or const_cast etc.). – valoh May 26 '13 at 22:30
  • 1
    values do not change. the variable containing them may. The impact on (and from) other scopes is exactly the desired feature here. an easily misused one, but desired nontheless. – Elazar May 26 '13 at 22:31
  • Not in the context of multithreading. With multithreading unintended writes mean race conditions and as lambdas are especially useful for task based systems I wonder why the handling of const correctness is so inconsistent (capture by const-value as default but no easy way for capture by const &). – valoh May 27 '13 at 18:57
  • There's no point in capturing an `int` by reference if you're not going to change it. A bigger class type, maybe. I guess if I really wanted to use a local class object not easily copied read-only, I would capture a `std::cref` by value, yes. – aschepler May 29 '13 at 16:59
  • 1
    Answer here: https://stackoverflow.com/questions/2835626/c0x-lambda-capture-by-value-always-const – Miloslaw Smyk Nov 11 '17 at 00:51

3 Answers3

7

Let's say you are capturing a pointer by value. The pointer itself is const, but access to the object it points to is not.

int i = 0;
int* p = &i;
auto l = [=]{ ++*p; };
l();
std::cout << i << std::endl;  // outputs 1

This lambda is equivalent to:

struct lambda {
    int* p;
    lambda(int* p_) : p(p_) {}
    void operator()() const { ++*p; }
};

The const on the operator()() makes usage of p equivalent to declaring it as:

int* const p;

Similar thing happens with a reference. The reference itself is "const" (in quotes because references cannot be reseated), but access to the object it refers to is not.

Nevin
  • 4,595
  • 18
  • 24
3

Captured references are also const. Or rather, references are always implicitly const -- there is no syntax in the language that allows you to change where a reference points to. a = 1; when a is a reference is not changing the reference, but changing the thing that the reference references.

When you talk about "const reference", I think you are confused. You are talking about "reference to const int" (const int &). The "const" there refers to the thing the reference points to, not the reference itself. It's analogous with pointers: with "pointer to const int" (const int *), the pointer itself is not const -- you can assign to a variable of this type all you want. A real "const pointer" would be int *const. Here, you cannot assign to something of this type; but you can modify the int it points to. Hence, the "const" for the pointer or reference is separate from the "const" for the thing it points to. You can also have a "const pointer to const int": const int *const.

newacct
  • 119,665
  • 29
  • 163
  • 224
  • 2
    Of course I know that references are always referencing always to the same object, what I meant was const reference as in reference to const value (fixed misleading subject). When using lambdas in the context of multithreading (task stealing system), unintended writes easily lead to race conditions. Imo with the concept of const correctness c++ has an advantage over other languages (like c#), but in the context of lambdas it seem to be a little bit messy. And I would like to know the reason why there is no easy way to have const lambdas where changed objects are explicit marked. – valoh May 27 '13 at 18:50
0

My logic says the following: lambdas are just a piece of code, with optional needed references. In the case when you need to actually copy something (which usually happens for memory management purposes, such as copying a shared_ptr), you still don't really want the lambda to have its own state. That's quite an unusual situation.

I think only the 2 following options feel "right"

  • Enclose some code using some local variables, so that you can "pass it around"
  • The same as above, only add memory management, because maybe you want to execute that piece of code asynchronously or something, and the creating scope will disappear.

But when a lambda is "mutable", i.e., it captures values which aren't const, this means that it actually supports its own state. Meaning, every time you call a lambda, it could yield a different result, which isn't based on its actual closure, but again, on its internal state, which is kind of counter-intuitive in my book, considering that lambdas originate in functional languages.

However, C++, being C++, gives you a way to bypass that limitation, by also making it a bit uglier, just to make sure you're aware of the fact you're doing something strange.

I hope this reasons with you.

Yam Marcovic
  • 7,953
  • 1
  • 28
  • 38
  • I totally understand the reasoning of why capture-by-values are const and totally agree that this it is a good idea to have it by default. But what I still don't get is the missing constness support for capture-by-reference, especially as this prevents robust lamba usage in multi threaded environments -> non-const == possible race condition so better be sure to only be non-const when you really mean it. – valoh May 27 '13 at 21:14
  • Oh. I'd say that's somewhat of an edge case. A lambda IMO deals with local variables as they are. If you added special syntax for each edge case, you'd end up with a monstrous langu... well, a *more* monstrous one, at least. As you can imagine, it's still possible to, functionally, get what you want to have here, by simply aliasing your variable to a const-ref of it, and then capturing that alias. – Yam Marcovic May 27 '13 at 21:18
  • Maybe I'm expecting too much :-) And actually I guess I phrased my original question very misunderstanding. I now would argue the same reasons for making capture-by-value being const apply to capture-by-reference. And for modifying a const object you always have the const cast making it really explicit what get's modified. At least support for [const&](){} doesn't sound too much. In the current form the const handling of capture-by-reference seem a little bit sloppy and doesn't support a clean and robust usage. – valoh May 27 '13 at 21:34
  • Well, I guess at this point it's way too subjective. Personally, at least judging by my experience with C++11, I'm pleased with most of their decisions. This one included. I'm actually missing "move closure" more than "const reference closure". And I'm also missing "auto" params in lambda, where they can be. – Yam Marcovic May 27 '13 at 21:45