Why does lambda auto& parameter choose const overload?

  • A+
Category:Languages

I'm trying to implement a class which wraps an arbitrary type and a mutex. To access the wrapped data, one needs to pass a functor as parameter of the locked method. The wrapper class will then pass the wrapped data as parameter to the functor.

I'd like my wrapper class to work with const & non-const, so I tried the following

#include <mutex> #include <string>  template<typename T, typename Mutex = std::mutex> class   Mutexed { private:     T m_data;     mutable Mutex m_mutex;  public:     using type = T;     using mutex_type = Mutex;  public:     explicit Mutexed() = default;      template<typename... Args>     explicit Mutexed(Args&&... args)         : m_data{std::forward<Args>(args)...}     {}      template<typename F>     auto locked(F&& f) -> decltype(std::forward<F>(f)(m_data)) {         std::lock_guard<Mutex> lock(m_mutex);         return std::forward<F>(f)(m_data);     }      template<typename F>     auto locked(F&& f) const -> decltype(std::forward<F>(f)(m_data)) {         std::lock_guard<Mutex> lock(m_mutex);         return std::forward<F>(f)(m_data);     } };  int main() {     Mutexed<std::string> str{"Foo"};      str.locked([](auto &s) { /* this doesn't compile */         s = "Bar";     });      str.locked([](std::string& s) { /* this compiles fine */         s = "Baz";     });     return 0; } 

The first locked call with the generic lambda fails to compile with the following error

/home/foo/tests/lamdba_auto_const/lambda_auto_const/main.cpp: In instantiation of ‘main()::<lambda(auto:1&)> [with auto:1 = const std::__cxx11::basic_string<char>]’: /home/foo/tests/lamdba_auto_const/lambda_auto_const/main.cpp:30:60:   required by substitution of ‘template<class F> decltype (forward<F>(f)(((const Mutexed<T, Mutex>*)this)->Mutexed<T, Mutex>::m_data)) Mutexed<T, Mutex>::locked(F&&) const [with F = main()::<lambda(auto:1&)>]’ /home/foo/tests/lamdba_auto_const/lambda_auto_const/main.cpp:42:6:   required from here /home/foo/tests/lamdba_auto_const/lambda_auto_const/main.cpp:41:11: error: passing ‘const std::__cxx11::basic_string<char>’ as ‘this’ argument discards qualifiers [-fpermissive]          s = "Bar";            ^ In file included from /usr/include/c++/5/string:52:0,                  from /usr/include/c++/5/stdexcept:39,                  from /usr/include/c++/5/array:38,                  from /usr/include/c++/5/tuple:39,                  from /usr/include/c++/5/mutex:38,                  from /home/foo/tests/lamdba_auto_const/lambda_auto_const/main.cpp:1: /usr/include/c++/5/bits/basic_string.h:558:7: note:   in call to ‘std::__cxx11::basic_string<_CharT, _Traits, _Alloc>& std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::operator=(const _CharT*) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]’        operator=(const _CharT* __s)        ^ 

But the second call with the std::string& parameter is fine.

Why is that ? And is there a way to make it work as expected while using a generic lambda ?

 


This is a problem fundamentally with what happens with SFINAE-unfriendly callables. For more reference, check out P0826.

The problem is, when you call this:

 str.locked([](auto &s) { s = "Bar"; }); 

We have two overloads of locked and we have to try both. The non-const overload works fine. But the const one – even if it won't be selected by overload resolution anyway – still has to be instantiated (it's a generic lambda, so to figure out what decltype(std::forward<F>(f)(m_data)) might be you have to instantiate it) and that instantiation fails within the body of the lambda. The body is outside of the immediate context, so it's not a substitution failure – it's a hard error.

When you call this:

str.locked([](std::string& s) { s = "Bar"; }); 

We don't need to look at the body at all during the whole process of overload resolution – we can simply reject at the call site (since you can't pass a const string into a string&).

There's not really a solution to this problem in the language today – you basically have to add constraints on your lambda to ensure that the instantiation failure happens in the immediate context of substitution rather than in the body. Something like:

str.locked([](auto &s) -> decltype(s = std::string(), void()) {     s = "Bar"; }); 

A more thorough language solution would have been to allow for "Deducing this" (see the section in the paper about this specific problem). But that won't be in C++20.

Comment

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: