Почему нельзя перебрасывать аргументы внутри лямбды без изменчивости? - PullRequest
15 голосов
/ 05 мая 2019

В приведенной ниже программе, когда mutable не используется, программа не может быть скомпилирована.

#include <iostream>
#include <queue>
#include <functional>

std::queue<std::function<void()>> q;

template<typename T, typename... Args>
void enqueue(T&& func, Args&&... args)
{
    //q.emplace([=]() {                  // this fails
    q.emplace([=]() mutable {             //this works
        func(std::forward<Args>(args)...);
    });
}

int main()
{
    auto f1 = [](int a, int b) { std::cout << a << b << "\n"; };
    auto f2 = [](double a, double b) { std::cout << a << b << "\n";};
    enqueue(f1, 10, 20);
    enqueue(f2, 3.14, 2.14);
    return 0;
}

Это ошибка компилятора

lmbfwd.cpp: In instantiation of ‘enqueue(T&&, Args&& ...)::<lambda()> [with T = main()::<lambda(int, int)>&; Args = {int, int}]’:
lmbfwd.cpp:11:27:   required from ‘struct enqueue(T&&, Args&& ...) [with T = main()::<lambda(int, int)>&; Args = {int, int}]::<lambda()>’
lmbfwd.cpp:10:2:   required from ‘void enqueue(T&&, Args&& ...) [with T = main()::<lambda(int, int)>&; Args = {int, int}]’
lmbfwd.cpp:18:20:   required from here
lmbfwd.cpp:11:26: error: no matching function for call to ‘forward<int>(const int&)’
   func(std::forward<Args>(args)...);

Я не могу понять, почему переадресация аргументов происходит без mutable.

Кроме того, если я передаю лямбду со строкой в ​​качестве аргумента, mutable не требуется, и программа работает.

#include <iostream>
#include <queue>
#include <functional>

std::queue<std::function<void()>> q;

template<typename T, typename... Args>
void enqueue(T&& func, Args&&... args)
{
   //works without mutable
    q.emplace([=]() {
        func(std::forward<Args>(args)...);
    });
}
void dequeue()
{
    while (!q.empty()) {
        auto f = std::move(q.front());
        q.pop();
        f();
    }
}
int main()
{
    auto f3 = [](std::string s) { std::cout << s << "\n"; };
    enqueue(f3, "Hello");
    dequeue();
    return 0;
}

Почему изменчивость требуется в случае int double, а не в случае string? В чем разница между этими двумя?

1 Ответ

19 голосов
/ 05 мая 2019

Лямбда, отличная от 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.

...