web-dev-qa-db-fra.com

Pourquoi un argument ne peut-il pas être transmis à l'intérieur de lambda sans mutable?

Dans le programme ci-dessous, lorsque mutable n'est pas utilisé, le programme ne parvient pas à compiler.

#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;
}

Ceci est l'erreur du compilateur

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)...);

Je ne parviens pas à comprendre pourquoi le transfert d'arguments échoue sans mutable.

De plus, si je passe un lambda avec une chaîne comme argument, mutable n'est pas requis et le programme fonctionne.

#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;
}

Pourquoi est-ce que mutable est requis en cas de int double et pas en cas de string? Quelle est la différence entre ces deux?

16
bornfree

Un lambda nonmutable génère un type de fermeture avec un qualificatif implicite const sur sa surcharge operator().

std::forward Est un mouvement conditionnel: il équivaut à std::move Lorsque l'argument de modèle fourni n'est pas une référence de valeur. Il est défini comme suit:

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;

(Voir: https://en.cppreference.com/w/cpp/utility/forward ).


Simplifions votre extrait de code pour:

#include <utility>

template <typename T, typename... Args>
void enqueue(T&& func, Args&&... args)
{
    [=] { func(std::forward<Args>(args)...); };
}

int main()
{
    enqueue([](int) {}, 10);
}

L'erreur produite par clang++ 8.x Est:

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
    ^

Dans l'extrait ci-dessus:

  • Args est int et fait référence au type en dehors du lambda.

  • args fait référence au membre de la fermeture synthétisé via la capture lambda, et est const en raison de l'absence de mutable.

Par conséquent, l'invocation de std::forward Est ...

std::forward<int>(/* `const int&` member of closure */)

... qui ne correspond à aucune surcharge existante de std::forward. Il existe une incompatibilité entre l'argument de modèle fourni à forward et son type d'argument de fonction.

L'ajout de mutable au lambda rend args non -const, et une surcharge appropriée de forward est trouvée (la première, qui déplace son argument).


En utilisant des captures d'extension de pack C++ 20 pour "réécrire" le nom de args, nous pouvons éviter la discordance mentionnée ci-dessus, rendant le code compilable même sans mutable:

template <typename T, typename... Args>
void enqueue(T&& func, Args&&... args)
{
    [func, ...xs = args] { func(std::forward<decltype(xs)>(xs)...); };
}

exemple en direct sur godbolt.org


Pourquoi mutable est-il requis dans le cas de int double Et pas dans le cas de string? Quelle est la différence entre ces deux?

C'est amusant - cela fonctionne parce que vous ne passez pas réellement un std::string Dans votre invocation:

enqueue(f3, "Hello");
//          ^~~~~~~
//          const char*

Si vous faites correspondre correctement le type de l'argument passé à enqueue à celui accepté par f3, Il cessera de fonctionner comme prévu (sauf si vous utilisez mutable ou C + +20 fonctionnalités):

enqueue(f3, std::string{"Hello"});
// Compile-time error.

Pour expliquer pourquoi la version avec const char* Fonctionne, regardons à nouveau un exemple simplifié:

template <typename T>
void enqueue(T&& func, const char (&arg)[6])
{
    [=] { func(std::forward<const char*>(arg)); };
}

int main()
{
    enqueue([](std::string) {}, "Hello");
}

Args est déduit comme const char(&)[6]. Il y a une surcharge forward correspondante:

template< class T >
constexpr T&& forward( typename std::remove_reference<T>::type&& t ) noexcept;

Après substitution:

template< class T >
constexpr const char*&& forward( const char*&& t ) noexcept;

Cela renvoie simplement t, qui est ensuite utilisé pour construire le std::string.

19
Vittorio Romeo