Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Behavior difference of lambda function mutable capture from a reference to global variable

Tags:

I found the results are different across compilers if I use a lambda to capture a reference to global variable with mutable keyword and then modify the value in the lambda function.

#include <stdio.h> #include <functional>  int n = 100;  std::function<int()> f() {     int &m = n;     return [m] () mutable -> int {         m += 123;         return m;     }; }  int main() {     int x = n;     int y = f()();     int z = n;      printf("%d %d %d\n", x, y, z);     return 0; } 

Result from VS 2015 and GCC (g++ (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609):

100 223 100 

Result from clang++ (clang version 3.8.0-2ubuntu4 (tags/RELEASE_380/final)):

100 223 223 

Why does this happen? Is this allowed by the C++ Standards?

like image 499
Willy Avatar asked Mar 27 '20 01:03

Willy


People also ask

Does lambda capture reference by value?

The mutable keyword is used so that the body of the lambda expression can modify its copies of the external variables x and y , which the lambda expression captures by value. Because the lambda expression captures the original variables x and y by value, their values remain 1 after the lambda executes.

What is capture in lambda function?

Captures default to const value. By default, variables are captured by const value . This means when the lambda is created, the lambda captures a constant copy of the outer scope variable, which means that the lambda is not allowed to modify them.

What is a lambda capture list?

The capture list defines the outside variables that are accessible from within the lambda function body. The only capture defaults are. & (implicitly capture the used automatic variables by reference) and. = (implicitly capture the used automatic variables by copy).

What is mutable C++?

C++Server Side ProgrammingProgramming. Mutable data members are those members whose values can be changed in runtime even if the object is of constant type. It is just opposite to constant. Sometimes logic required to use only one or two data member as a variable and another one as a constant to handle the data.


2 Answers

A lambda can't capture a reference itself by value (use std::reference_wrapper for that purpose).

In your lambda, [m] captures m by value (because there is no & in the capture), so m (being a reference to n) is first dereferenced and a copy of the thing it is referencing (n) is captured. This is no different than doing this:

int &m = n; int x = m; // <-- copy made! 

The lambda then modifies that copy, not the original. That is what you are seeing happen in the VS and GCC outputs, as expected.

The Clang output is wrong, and should be reported as a bug, if it hasn't already.

If you want your lambda to modify n, capture m by reference instead: [&m]. This is no different than assigning one reference to another, eg:

int &m = n; int &x = m; // <-- no copy made! 

Or, you can just get rid of m altogether and capture n by reference instead: [&n].

Although, since n is in global scope, it really doesn't need to be captured at all, the lambda can access it globally without capturing it:

return [] () -> int {     n += 123;     return n; }; 
like image 128
Remy Lebeau Avatar answered Oct 21 '22 10:10

Remy Lebeau


I think Clang may actually be correct.

According to [lambda.capture]/11, an id-expression used in the lambda refers to the lambda's by-copy-captured member only if it constitutes an odr-use. If it doesn't, then it refers to the original entity. This applies to all C++ versions since C++11.

According to C++17's [basic.dev.odr]/3 a reference variable is not odr-used if applying lvalue-to-rvalue conversion to it yields a constant expression.

In the C++20 draft however the requirement for the lvalue-to-rvalue conversion is dropped and the relevant passage changed multiple times to include or not include the conversion. See CWG issue 1472 and CWG issue 1741, as well as open CWG issue 2083.

Since m is initialized with a constant expression (referring to a static storage duration object), using it yields a constant expression per exception in [expr.const]/2.11.1.

This is not the case however if lvalue-to-rvalue conversions are applied, because the value of n is not usable in a constant expression.

Therefore, depending on whether or not lvalue-to-rvalue conversions are supposed to be applied in determining odr-use, when you use m in the lambda, it may or may not refer to the member of the lambda.

If the conversion should be applied, GCC and MSVC are correct, otherwise Clang is.

You can see that Clang changes it behavior if you change the initialization of m to not be a constant expression anymore:

#include <stdio.h> #include <functional>  int n = 100;  void g() {}  std::function<int()> f() {     int &m = (g(), n);     return [m] () mutable -> int {         m += 123;         return m;     }; }  int main() {     int x = n;     int y = f()();     int z = n;      printf("%d %d %d\n", x, y, z);     return 0; } 

In this case all compilers agree that the output is

100 223 100 

because m in the lambda will refer to the closure's member which is of type int copy-initialized from the reference variable m in f.

like image 39
walnut Avatar answered Oct 21 '22 09:10

walnut