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?
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 deint double
Et pas dans le cas destring
? 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
.