2
votes

Comment utiliser correctement un appelable passé par une référence de transfert?

J'ai l'habitude de passer des fonctions lambda (et d'autres callables) aux fonctions de modèle - et de les utiliser - comme suit

template <typename F>
auto bar (F && f)
 {
   using A = typename decltype(std::function{std::forward<F>(f)})::result_type;

   std::vector<A>  vect;

   for ( auto i { 0u }; i < 10u ; ++i )
      vect.push_back(std::forward<F>(f)());

   return vect;
 }

Je veux dire: j'ai l'habitude de les passer une référence de transfert et appelez-les en passant par std::forward.

Un autre utilisateur de Stack Overflow fait valoir (voir les commentaires sur this answer ) que ceci, en appelant le fonctionnel deux fois ou plus, c'est dangereux parce que c'est sémantiquement invalide et potentiellement dangereux (et peut-être aussi un comportement indéfini) lorsque la fonction est appelée avec une valeur r référence.

J'ai partiellement mal compris ce qu'il veut dire (ma faute) mais le doute restant est si la fonction bar () suivante (avec un multiple indubitable std: : transférer sur le même objet) c'est du code correct ou c'est (peut-être seulement potentiellement) dangereux.

template <typename F>
auto foo (F && f)
 {
   // ...

   auto x = std::forward<F>(f)(/* some arguments */);

   // ... 
 }


0 commentaires

3 Réponses :


4
votes

La progression n'est qu'un mouvement conditionnel.

Par conséquent, transférer plusieurs fois la même chose est, en général, aussi dangereux que de s'en éloigner plusieurs fois.

Les avants non évalués ne déplacent rien, donc ceux-ci ne comptent pas.

Le routage via std :: function ajoute une ride: cette déduction ne fonctionne que sur les pointeurs de fonction et sur les objets de fonction avec un seul opérateur d'appel de fonction qui n'est pas qualifié && . Pour ceux-ci, les invocations de rvalue et lvalue sont toujours équivalentes si les deux se compilent.


0 commentaires

1
votes

Si vous allez simplement transférer l'appelable vers un autre endroit ou simplement appeler l'appelable exactement une fois, je dirais que l'utilisation de std :: forward est la bonne chose à faire en général. Comme expliqué ici , cela préservera en quelque sorte la catégorie de valeur de l'appelable et permettra d'appeler la version "correcte" d'un opérateur d'appel de fonction potentiellement surchargé.

Le problème dans le thread d'origine était que l'appelable était appelé dans une boucle, donc potentiellement invoquée plus d'une fois. L'exemple concret de l'autre thread était

class MyFancyCallable
{
public:
    void operator () & { /* do some stuff */ }
    void operator () && { /* do some stuff in a special way that can only be done once */ }
};

Ici, je crois que l'appel de std :: forward (f) (element) à la place de f (élément) , c'est-à-dire

template <typename F>
auto map(F&& f) const
{
    using output_element_type = decltype(std::forward<F>(f)(std::declval<T>()));

    auto sequence = std::make_unique<Sequence<output_element_type>>();

    for (const T& element : *this)
        sequence->push(std::forward<F>(f)(element));

    return sequence;
}

serait potentiellement problématique. Pour autant que je sache, la caractéristique déterminante d'une valeur r est qu'elle ne peut pas être explicitement mentionnée. En particulier, il n'y a naturellement aucun moyen d'utiliser la même valeur prvalue plus d'une fois dans une expression (du moins je ne peux pas penser à une seule). De plus, pour autant que je sache, si vous utilisez std :: move ou std :: forward ou tout autre moyen d'obtenir une valeur x, même sur le même objet d'origine, le résultat sera une nouvelle valeur x à chaque fois. Ainsi, il ne peut pas non plus être possible de faire référence à la même valeur x plus d'une fois. Étant donné que la même rvalue ne peut pas être utilisée plus d'une fois, je dirais (voir également les commentaires sous cette réponse ) que ce serait généralement être une chose valable pour un opérateur d'appel de fonction surchargé de faire quelque chose qui ne peut être fait qu'une seule fois au cas où l'appel se produirait sur une rvalue, par exemple:

template <typename F>
auto map(F&& f) const
{
    using output_element_type = decltype(f(std::declval<T>()));

    auto sequence = std::make_unique<Sequence<output_element_type>>();

    for (const T& element : *this)
        sequence->push(f(element));

    return sequence;
}

L'implémentation de MyFancyCallable peut supposer qu'un appel qui choisirait la version qualifiée && ne peut pas se produire plus d'une fois (sur l'objet donné). Ainsi, je considérerais que transférer le même appelable dans plus d'un appel est sémantiquement interrompu.

Bien sûr, techniquement, il n'y a pas de définition universelle de ce que signifie réellement avancer ou déplacer un objet. En fin de compte, c'est vraiment à l'implémentation des types particuliers impliqués d'y attribuer un sens. Ainsi, vous pouvez simplement spécifier dans le cadre de votre interface que les appelables potentiels passés à votre algorithme doivent être capables d'être appelés plusieurs fois sur une rvalue qui fait référence au même objet. Cependant, cela va à peu près à l'encontre de toutes les conventions sur la façon dont le mécanisme de référence rvalue est généralement utilisé en C ++, et je ne vois pas vraiment ce qu'il y aurait à gagner à faire cela ...


2 commentaires

Un de ces siècles, je comprendrai la transmission parfaite. Bonne réponse mais celle de HolyBlackCat nous donne un exemple simple mais clair du problème pratique. Quoi qu'il en soit, vous avez raison: merci pour votre patience.


@ max66, vous pouvez consulter ce site si vous ne l'avez pas déjà fait. Cela donne une assez belle explication de tout cela.



2
votes

Je dirais que la règle générale s'applique dans ce cas. Vous n'êtes pas censé faire quoi que ce soit avec une variable après qu'elle a été déplacée / transférée, sauf peut-être lui assigner.

Ainsi ...

Comment utiliser correctement un appelable passé par une référence de transfert?

Ne transférez que si vous êtes sûr qu'il ne sera pas rappelé (c'est-à-dire lors du dernier appel, le cas échéant).

S'il n'est jamais appelé plus d'une fois, il n'y a pas raison de ne pas transférer.


Quant à savoir pourquoi votre extrait de code pourrait être dangereux, pensez au foncteur suivant:

template <typename A, typename F>
auto bar (F && f)
 {    
   std::vector<A>  vect;

   for ( auto i { 0u }; i < 10u ; ++i )
      vect.push_back(std::forward<F>(f)());

   return vect;
 }

En guise d'optimisation, operator () lorsqu'il est appelé sur une rvalue permet à l'appelant de se déplacer de value.

Maintenant, votre modèle ne se compilerait pas si on lui donnait ce foncteur (car, comme TC dit, std :: function ne pourrait pas déterminer le type de retour dans ce cas).

Mais si nous le changions un peu:

template <typename T>
struct foo
{
    T value;
    const T &operator()() const & {return value;}
    T &&operator()() && {return std::move(value);}
};

alors il se briserait de façon spectaculaire lorsqu'on lui donnait ce foncteur.


1 commentaires

Exemple très agréable et instructif.