C++ Logo

std-discussion

Advanced search

lambda, capture by const-ref

From: Federico Kircheis <federico.kircheis_at_[hidden]>
Date: Wed, 15 Sep 2021 23:05:06 +0200
Hello to everyone.

I hope this is the right mailing list, if not excuse me.


I wanted to gather opinions about lambda captures, and understand if it
would make sense to write a paper.

First of all, the use case.

Suppose I have a class that looks like like


----
struct data{
     std::string str = "Hello World!";
     int i = -1;
};
class data_and_mutex{
     data d; // data protected by mutex
     mutable std::mutex m;
     public:
     template <class F>
     auto lock(F f){
         std::scoped_lock lck(this->m);
         return f(this->d);
     }
};
----
Notice that it provides a lock member function for providing access to 
it's internal data, and at the same time ensure that the mutex is always 
locked.
(I like this pattern as it is, for the user of the class, very hard to 
misuse by accident, and at the same time leaves great flexibility.
For example it's the caller that decides the atomicity/granularity of 
the operations, without introducing an additional locking mechanisms.)
This is one of the uses of the API:
----
bool foo(data_and_mutex& d_m){
     return d_m.lock([](data& d){
         bool res = d.str == get_value();
         if(res){d.i++;}
         return res;
     } );
}
----
`get_value` is a function declared somewhere, we might even not control 
it, it's signature is one of the following, it should not matter which:
----
const std::string& get_value();
// or
std::string get_value();
// could also be std::string& get_value(), but leaving out for simplicity
----
What happens inside function foo is suboptimal at least, possibly 
problematic, as I'm calling external code (get_value) while locking a mutex.
A fix is simple
----
bool foo(data_and_mutex& d_m){
     const auto& value = get_value();
     return d_m.lock([&](data& d){
         bool res = d.str == value;
         if(res){d.i++;}
         return res;
     } );
}
----
Note:
  * I'm aware that technical std::string::operator== is still external 
code, but I know/assume it's a pure/non-problematic function and won't 
lead to deadlocks or lead to problematic performance issues
  * `const auto&` (or `const std::string&`) works correctly with both 
possible signatures of get_value
In this case, `foo` does not do much work, but we expanded a lot the 
scope of the variable returned by get_value.
Fortunately, since c++14, we can capture the values returned by 
functions directly.
Thus it is possible to write
----
bool foo(data_and_mutex& d_m){
     return d_m.lock([value = get_value()](data& d){
         bool res = d.str == value;
         if(res){d.i++;}
         return res;
     } );
}
----
This seems perfect, because get_value is called before holding the 
mutex, but...
If the signature is "const std::string& get_value()", the we are 
creating an unnecessary copy(!), while in all previous snippets we did not.
To avoid this copy, one needs to write (notice the &)
----
bool foo(data_and_mutex& d_m){
     return d_m.lock([&value = get_value()](data& d) {
         bool res = d.str == value;
         if(res){d.i++;}
         return res;
     } );
}
----
But this code does not work with `std::string get_value();`, it fails to 
compile with "error: non-const lvalue reference to type 
'basic_string<...>' cannot bind to a temporary of type 
'basic_string<...>'" as it tries to make a mutable(!) reference.
Unfortunately we cannot write something like
----
bool foo(data_and_mutex& d_m){
     return d_m.lock([const& value = get_value()](data& d) {
         bool res = d.str == value;
         if(res){d.i++;}
         return res;
     } );
}
----
I know std::as_const is normally proposed as solution for similar 
issues, but
  * it does not compile for rvalues
  * even if it would compile, it does not enable the life extension by 
const-ref, and thus would create a dangling `value`
On could do what std::as_const does by hand, ie casting to const
----
bool foo(data_and_mutex& d_m){
     return d_m.lock([&value = static_cast<const 
std::string>(get_value())](data& d) {
         bool res = d.str == value;
         if(res){d.i++;}
         return res;
     } );
}
----
which works correctly, but it seems verbose, and makes it necessary to 
spell the type returned from get_value. thus making it possible to add 
accidental conversions.
AFAIK, a macro is the only viable approach (as a function does not work) 
to reduce both verbosity and risk of unwanted conversions:
----
#define C_REF(...) static_cast<const decltype(__VA_ARGS__)&>(__VA_ARGS__)
bool foo(data_and_mutex& d_m){
      return d_m.lock([&value = C_REF(get_value())](data& d) {
          bool res = d.str == value;
          if(res){d.i++;}
          return res;
      } );
}
----
nevertheless the code is (slightly) more verbose (in my examples I've 
used only a get_value function, imagine having two or three) and is not 
as idiomatic.
Using `const &` is a pattern we already use, and would thus feel more 
natural.
Also introducing a macro seems/feels... wrong.
(And once I have/need this macro... what value does std::as_const add?)
So, to sum it up, I would like see the possibility to add const to the 
lambda capture.
Just this qualifier (leaving thus volatile out), and not the type (it 
can be done independently if we acknowledge it might be a good idea).
This would give us a "unified syntax" that works both for function 
returning values and references (both const and mutable), as long as we 
do not want to modify the value.
This is actually what we already have when working with functions:
* void foo(const std::string&); vs void foo(std::string);
in both cases the user of foo can write foo(std::string("hello"));
* std::string foo(); vs const std::string& foo();
in both cases the user can write
std::string v = foo();
or(!)
const std::string& v = foo();
(or use auto instead of std::string)
The surrounding code normally does not need any modification because 
`const auto&` binds to values, so we can write the code once for both cases.
I would like to know (supposing I'm on the correct mailing list) some 
opinions about the topic, and if it is worth writing and presenting a paper.
(NOTE: I've only written a couple of papers for LEWG, I've read that 
writing papers and getting it accepted for core is more difficult)
Best
Federico

Received on 2021-09-15 16:08:48