Un usage de std::ref
Je repars aujourd’hui de ce post sur std::reference_wrapper, pour expliquer l’intérêt de std::ref dans les appels de fonctions template.
Dans un précédent post, on a lancé un calcul lourd dans un thread détaché du thread principal comme ceci :
void heavy_task(int& result, const int amount) { /* ... */ }
int main(void)
{
int result_even = 0;
auto th = std::thread(heavy_task, std::ref(result_even), amount);
}
On s’intéresse à std::ref(result_even) : qu’apporte std::ref, et pourquoi est-il nécessaire ici ?
ce qu’en dit la doc
Commençons par voir ce que cppreference dit de std::ref :
Function templates ref and cref are helper functions that generate an object of type std::reference_wrapper.
Et effectivement, on voit dans le prototype de std::ref<T> qu’elle renvoie un std::reference_wrapper<T> :
template< class T >
std::reference_wrapper<T> ref(T& t) noexcept;
Ok, on a juste décalé notre question, voyons ce que dit cppreference au sujet des std::reference_wrapper. C’est un peu plus verbeux, pour le sujet du jour, on va se limiter à ça :
std::reference_wrapper is a class template that wraps a reference in a copyable, assignable object.
(…)
Instances of std::reference_wrapper (…) are implicitly convertible to T&, so that they can be used as arguments with the functions that take the underlying type by reference.
conversion implicite en T&
Considérons cette fonction template, dont l’objectif est d’incrémenter de 100 tout ce qu’on lui donne à manger :
template <typename T>
void add100(T x) { x += 100; }
Question : peut-on l’utiliser comme ceci ? Que va afficher le code ci-dessous, 142 ?
int a = 42;
add100(a);
std::cout << a;
Non bien sûr : l’argument template est int, a est passé à la fonction add100 par copie, et c’est bien une copie locale de a qui vaut 142 : a conserve sa valeur initiale.
On pourrait alors être tenté de l’utiliser comme ça. Que va afficher le code ci-dessous, 142 ?
int a = 42;
int& ref_to_a = a;
add100(ref_to_a);
std::cout << a;
Toujours pas ! L’argument-template reste int, et a conserve ici aussi sa valeur initiale…
Pour permettre à add100 de muter a, il faut indiquer explicitement que le type template est int& : le code suivant mute a et affiche bien 142 :
int a = 42;
int& ref_to_a = a;
add100<int&>(ref_to_a);
std::cout << a;
// finally displays 142
Ce comportement peut paraître surprenant au premier abord, mais c’est ainsi que fonctionne la déduction de type pour les fonctions template : une référence vers une variable de type T est toujours déduite comme T, et non comme T&. Un exemple simple pour résumer :
template <typename T> void pouet(T) {}
template <> void pouet(int) { std::cout << "int"; }
template <> void pouet(int&) { std::cout << "int&"; }
int main(void)
{
int x = 42;
int& x_ref = x;
pouet(x); // int
pouet(x_ref); // int !
pouet<int&>(x_ref); // int&
}
et std::ref dans tout ça ?
Il se trouve que laisser le compilateur faire la déduction de type est pratique : les types en C++ peuvent vite devenir imbittables, ça peut être galère (voire même impossible, cf. plus bas) de préciser explicitement les types à chaque appel de fonction template :
// argh
do_something_clever<std::map<std::string, std::pair<unsigned int, std::vector<std::thread> > > & >(thread_groups);
// le template-type est passé explicitement ... ... juste pour pouvoir mettre ça ^
Alors certes, on a toujours les typedef/using, mais ce qu’on aimerait avoir, c’est un moyen de "forcer" la déduction de type à T&… et à la surprise de personne au vu du titre du post, c’est justement ce que permet std::ref. Ceci est tout de même plus lisible :
do_something_clever(std::ref(thread_groups));
Pour revenir à notre exemple haut :
int a = 42;
add100(std::ref(a));
std::cout << a;
// ok, also displays 142
Dans code ci-dessus, add100 mute bien a, et on affiche correctement 142.
exemple 1 : std::bind
Un exemple classique où std::ref est utile : pour passer un argument à std::bind, qui sert à obtenir un foncteur en "figeant" certains arguments d’une fonction de base un peu trop configurable.
Supposons qu’on ait un container de gros objets, et qu’on souhaite supprimer avec std::remove_if ceux qui sont trop proches (disons à 20% près) d’une valeur pivot. On souhaite réutiliser une fonction is_approx_equal permettant de comparer deux BigObject, à un facteur de tolérance près :
struct BigObject { /*...*/ };
bool is_approx_equal(BigObject const& left, BigObject const& right, float tolerance) {/*...*/}
float tolerance = 0.2;
BigObject pivot = /* creates or get a pivot */;
vector<BigObject> vec = get_big_objects();
// ce qu'on aimerait faire :
auto past_the_end = remove_if(
vec.begin(),
vec.end(),
/* is_approx_equal(pivot, element, tolerance) */
);
Comme remove_if a besoin d’un opérateur unaire, dont le seul paramètre est l’élément du container en cours de parcours, il faut "figer" les deux autres paramètres de is_approx_equal, à savoir le pivot et le facteur de tolérance. Même si dans ce cas on utiliserait sans doute plutôt une lambda, std::bind est fait pour ça :
auto remove_checker = bind(is_approx_equal, placeholders::_1, pivot, tolerance);
auto past_the_end = remove_if(vec.begin(), vec.end(), remove_checker);
Problème : avec le code-ci-dessus, pivot, coûteux à copier, est passé par copie à chaque appel de is_approx_equal. Pour le passer par référence, on utilise std::ref, ou plutôt son équivalent pour les références constantes std::cref :
auto remove_checker = bind(is_approx_equal, placeholders::_1, std::cref(pivot), tolerance);
auto past_the_end = remove_if(vec.begin(), vec.end(), remove_checker);
Détail rigolo : il semblerait que le comportement de bind soit implémenté dans la libstdc++ par une template-specialization du cas où l’argument est un reference_wrapper.
exemple 2 : std::thread
Une autre situation où on utilise std::ref, donné au début de ce post : la création d’un std::thread :
void heavy_task(int& result)
{
result = 42;
}
int main(void)
{
int result = 0;
int& ref_to_result = result;
// auto th = std::thread(heavy_task, result); // won't compile
// auto th = std::thread(heavy_task, ref_to_result); // won't compile
auto th = std::thread(heavy_task, std::ref(result)); // ok
}
Ici, heavy_task attend un int&, et std::ref permet de le lui passer.
Le cas de std::thread est même particulier : la classe std::thread n’est PAS une classe template, mais elle dispose d’un constructeur template acceptant une fonction et ses arguments :
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );
Or, le C++ ne propose pas de syntaxe permettant une instanciation explicite d’un constructeur template : on est obligé de se reposer sur la template argument deduction… Par conséquent, il n’est pas possible comme pour les exemples avec add100 ci-dessous d’indiquer explicitement le int& comme argument template explicite :
auto th = std::thread<void (*)(int&), int&>(heavy_task, result); // won't compile
En effet, le code ci-dessus ne compile pas, car il est interprété comme l’instanciation de la classe template std::thread, alors que std::thread n’est PAS une classe template : c’est une classe "normale", dont un membre (son constructeur) est template.
résumé et autre usage
En résumé, std::ref créée un std::reference_wrapper, qui permet la déduction template de T&. Ça simplifie grandement les appels templates, voire, ça les rend possibles tout court dans le cas de std::thread.
Signalons, même si ce n’est pas le focus de ce post, que les reference_wrapper sont utiles dans d’autres situations, notamment pour faire des conteneurs de références.
En effet, T& n’est pas Erasable, or c’est un requirement du template parameter de std::vector. Du coup le code suivant ne compilera pas :
vector<int&> v; // won't compile
À la place, on peut utiliser un std::reference_wrapper :
int a=42, b=19;
std::vector<std::reference_wrapper<int>> v{a, b};
v[1] += 1000;
std::cout << b; // 1019