J'essaie d'implémenter une classe qui encapsule un type arbitraire et un mutex. Pour accéder aux données encapsulées, il faut passer un objet fonction comme paramètre de la méthode locked
. La classe wrapper transmettra ensuite les données encapsulées en tant que paramètre à cet objet fonction.
J'aimerais que ma classe wrapper fonctionne avec const et non const, j'ai donc essayé ce qui suit
#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;
}
Le premier appel locked
avec le lambda générique ne parvient pas à se compiler avec l'erreur suivante
/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)
^
Mais le deuxième appel avec le std::string&
le paramètre est correct.
Pourquoi donc ? Et existe-t-il un moyen de le faire fonctionner comme prévu lors de l'utilisation d'un lambda générique?
C'est un problème fondamental avec ce qui se passe avec les callables SFINAE hostiles. Pour plus de référence, consultez P0826 .
Le problème est que lorsque vous appelez ceci:
str.locked([](auto &s) { s = "Bar"; });
Nous avons deux surcharges de locked
et nous devons essayer les deux. La surcharge non - const
fonctionne correctement. Mais celui const
- même s'il ne sera pas sélectionné par la résolution de surcharge de toute façon - doit encore être instancié (c'est un lambda générique, donc pour comprendre ce que decltype(std::forward<F>(f)(m_data))
pourrait être vous avez pour l'instancier) et cette instanciation échoue dans le corps du lambda. Le corps est en dehors du contexte immédiat, donc ce n'est pas un échec de substitution - c'est une erreur difficile.
Lorsque vous appelez cela:
str.locked([](std::string& s) { s = "Bar"; });
Nous n'avons pas du tout besoin de regarder le corps pendant tout le processus de résolution de surcharge - nous pouvons simplement rejeter sur le site de l'appel (car vous ne pouvez pas passer un const string
Dans un string&
).
Il n'y a pas vraiment de solution à ce problème dans le langage aujourd'hui - vous devez essentiellement ajouter des contraintes sur votre lambda pour vous assurer que l'échec de l'instanciation se produit dans le contexte immédiat de substitution plutôt que dans le corps. Quelque chose comme:
str.locked([](auto &s) -> void {
s = "Bar";
});
Notez que nous n'avons pas besoin de rendre ce SFINAE-friendly - nous devons juste nous assurer que nous pouvons déterminer le type de retour sans instancier le corps.
Une solution linguistique plus approfondie aurait été de permettre la "déduction this
" (voir la section dans le document sur ce problème spécifique). Mais ce ne sera pas en C++ 20.