Почему лямбда-авто и параметр выбирают постоянную перегрузку?
Я пытаюсь реализовать класс, который оборачивает произвольный тип и мьютекс. Чтобы получить доступ к обернутым данным, необходимо передать объект функции в качестве параметра locked
метода. Затем класс-оболочка передаст упакованные данные в качестве параметра этому объекту функции.
Я хотел бы, чтобы мой класс-оболочка работал с const & non-const, поэтому я попробовал следующее
#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;
}
Первый locked
вызов с общей лямбдой не компилируется со следующей ошибкой
/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)
^
Но второй вызов с параметром std::string&
в порядке.
Это почему? И есть ли способ заставить его работать так, как ожидается, используя универсальную лямбду?
Ответы
Ответ 1
Это проблема в основном с тем, что происходит с недовольными вызовами SFINAE. Для получения дополнительной информации, проверьте P0826.
Проблема в том, когда вы называете это:
str.locked([](auto &s) { s = "Bar"; });
У нас две locked
перегрузки, и мы должны попробовать обе. Перегрузка non- const
работает нормально. Но const
- даже если он в любом случае не будет выбран разрешением перегрузки - все еще должен быть создан (это общая лямбда, так что нужно выяснить, что такое decltype(std::forward<F>(f)(m_data))
может быть, вам нужно создать его экземпляр), и этот экземпляр не срабатывает в теле лямбды. Тело находится вне непосредственного контекста, поэтому это не ошибка замещения - это серьезная ошибка.
Когда вы звоните это:
str.locked([](std::string& s) { s = "Bar"; });
Нам не нужно смотреть на тело в течение всего процесса разрешения перегрузки - мы можем просто отклонить сайт вызова (так как вы не можете передать const string
в string&
).
На сегодняшний день не существует решения этой проблемы в языке - в основном вам нужно добавить ограничения на вашу лямбду, чтобы гарантировать, что сбой создания экземпляра произойдет в непосредственном контексте замещения, а не в теле. Что-то вроде:
str.locked([](auto &s) -> void {
s = "Bar";
});
Обратите внимание, что нам не нужно делать этот SFINAE-дружественным - нам просто нужно убедиться, что мы можем определить тип возвращаемого значения без создания экземпляра тела.
Более полное языковое решение состояло бы в том, чтобы допустить "вывод this
" (см. Раздел в статье об этой конкретной проблеме). Но этого не будет в С++ 20.