Лямбда, отличная от mutable
, генерирует тип закрытия с неявным квалификатором const
при перегрузке operator()
.
std::forward
- это условный ход: он эквивалентен std::move
, если предоставленный аргумент шаблона не является ссылкой на значение . Он определяется следующим образом:
template< class T >
constexpr T&& forward( typename std::remove_reference<T>::type& t ) noexcept;
template< class T >
constexpr T&& forward( typename std::remove_reference<T>::type&& t ) noexcept;
(см .: https://en.cppreference.com/w/cpp/utility/forward).
Давайте упростим ваш фрагмент до:
#include <utility>
template <typename T, typename... Args>
void enqueue(T&& func, Args&&... args)
{
[=] { func(std::forward<Args>(args)...); };
}
int main()
{
enqueue([](int) {}, 10);
}
Ошибка, выданная clang++ 8.x
:
error: no matching function for call to 'forward'
[=] { func(std::forward<Args>(args)...); };
^~~~~~~~~~~~~~~~~~
note: in instantiation of function template specialization 'enqueue<(lambda at wtf.cpp:11:13), int>' requested here
enqueue([](int) {}, 10);
^
note: candidate function template not viable: 1st argument ('const int')
would lose const qualifier
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
^
note: candidate function template not viable: 1st argument ('const int')
would lose const qualifier
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
^
В приведенном выше фрагменте:
Args
равно int
и относится к типу вне лямбды.
args
относится к члену замыкания, синтезированному с помощью лямбда-захвата, и равен const
из-за отсутствия mutable
.
Следовательно, std::forward
вызов ...
std::forward<int>(/* `const int&` member of closure */)
... что не соответствует существующей перегрузке std::forward
. Существует несоответствие между аргументом шаблона, предоставленным forward
, и его типом аргумента функции.
Добавление mutable
к лямбде делает args
non- const
, и найдена подходящая перегрузка forward
(первая, которая перемещает свой аргумент).
Используя C ++ 20 захватов расширения пакета для «перезаписи» имени args
, мы можем избежать упомянутого выше несоответствия, делая код компилируемым даже без mutable
:
template <typename T, typename... Args>
void enqueue(T&& func, Args&&... args)
{
[func, ...xs = args] { func(std::forward<decltype(xs)>(xs)...); };
}
живой пример на godbolt.org
Почему mutable
требуется в случае int double
, а не string
? В чем разница между этими двумя?
Это забавно - это работает, потому что вы на самом деле не передаете std::string
в своем вызове:
enqueue(f3, "Hello");
// ^~~~~~~
// const char*
Если вы правильно сопоставите тип аргумента, переданного enqueue
, с аргументом, принятым f3
, он перестанет работать должным образом (если вы не используете функции mutable
или C ++ 20):
enqueue(f3, std::string{"Hello"});
// Compile-time error.
Чтобы объяснить, почему работает версия с const char*
, давайте снова посмотрим на упрощенный пример:
template <typename T>
void enqueue(T&& func, const char (&arg)[6])
{
[=] { func(std::forward<const char*>(arg)); };
}
int main()
{
enqueue([](std::string) {}, "Hello");
}
Args
выводится как const char(&)[6]
. Существует соответствующая перегрузка forward
:
template< class T >
constexpr T&& forward( typename std::remove_reference<T>::type&& t ) noexcept;
После замены:
template< class T >
constexpr const char*&& forward( const char*&& t ) noexcept;
Это просто возвращает t
, который затем используется для построения std::string
.