2016-09-04 1 views
11

Lorsque vous utilisez des références de transfert, est-ce une mauvaise idée de transférer la même valeur à plusieurs fonctions à ? Considérez le morceau de code suivant:Renvoyer la même valeur à deux fonctions ou plus

template<typename Container> 
constexpr auto 
front(Container&& c) 
-> typename Container::value_type 
{ return std::forward<Container>(c).front(); } 

template<typename Container> 
constexpr auto 
back(Container&& c) 
-> typename Container::value_type 
{ return std::forward<Container>(c).back(); } 

template<typename Container> 
constexpr auto 
get_corner(Container&& c) 
{ 
    return do_something(front(std::forward<Container(c)), 
         back(std::forward<Container>(c)); 
} 

Si Container est une lvalue référence, la fonction fonctionne très bien. Cependant, je m'inquiète des situations où les valeurs lui sont transmises, car la valeur serait invalidée une fois qu'une opération de déplacement a eu lieu. Mon doute est: Y a-t-il un moyen correct d'expédier le conteneur dans ce cas, sans perdre la catégorie de valeur?

+0

"est-ce une mauvaise idée de transmettre la même valeur à plus d'une fonction? Considérons le morceau de code suivant: "En général, oui. Dans certains cas d'utilisation, c'est inoffensif. Cela ne peut que vous blesser, vraiment. Dans votre cas d'utilisation spécifique, pourquoi transmettre du tout? – AndyG

+0

@AndyG, déplacer la sémantique. –

Répondre

13

En général, il n'est pas raisonnable que la même fonction transfère deux fois le même paramètre. Pas à moins d'avoir une connaissance spécifique de ce que fera le récepteur de ce paramètre transféré. Rappelez-vous que le comportement de std::forward peut être équivalent au comportement std::move, en fonction du paramètre passé par l'utilisateur. Le comportement d'une valeur x dépendra de la façon dont la fonction de réception la traite. Si le destinataire prend une référence de valeur non-const, il va probablement déplacer de cette valeur si possible. Cela vous laisserait tenir un objet déplacé. S'il prend une valeur, il va certainement se déplacer si le type le prend en charge.

Donc, sauf si vous avez connaissance spécifique du comportement attendu des opérations que vous utilisez, il n'est pas sûr de transférer un paramètre plus d'une fois.

+0

Donc la bonne chose à faire ici est de n'accepter qu'une référence const lvalue à un conteneur? –

+4

@thist: Ici, parce que ni 'front()' ni 'back()' ne devraient jamais effectuer de déplacement, il est correct de leur transmettre une référence const lvalue. En général, la bonne approche est 'func1 (rvaluerefparam); func2 (rvaluerefparam); func3 (std :: forward (rvaluerefparam)); 'C'est-à-dire que seul le dernier utilisateur vole les ressources. –

+1

@thlst: 'forward' est seulement utile pour passer en toute transparence les références et les références de valeur r; Si vous limitez simplement le problème à des références sans valeur r (pour empêcher les déplacements), vous pouvez transmettre la même référence (const ou non) à plusieurs fonctions. Si mutable, l'ordre dans lequel les fonctions sont appelées est important. –

4

Il y a en fait aucune version rvalue référence de std::begin - nous venons de (mettre de côté constexpr et les valeurs de retour):

template <class C> 
??? begin(C&); 

template <class C> 
??? begin(C const&); 

Pour les conteneurs lvalue, vous obtenez iterator, et pour les conteneurs rvalue, vous obtenez const_iterator (ou quel que soit l'équivalent spécifique au conteneur). Le vrai problème de votre code est decltype(auto). Pour les conteneurs lvalue, c'est bien - vous retournerez une référence à un objet dont la durée de vie dépasse la fonction. Mais pour les conteneurs rvalue, cela renvoie une référence qui pend. Vous devez renvoyer une référence pour les conteneurs lvalue et une valeur pour les conteneurs rvalue.

En plus de cela, forward -sing les conteneurs dans begin()/end() n'est probablement pas ce que vous voulez faire. Il serait plus efficace de conditionner le résultat du select() en tant qu'élément de déplacement. Quelque chose comme this answer of mine:

template <typename Container, 
      typename V = decltype(*std::begin(std::declval<Container&>())), 
      typename R = std::conditional_t< 
       std::is_lvalue_reference<Container>::value, 
       V, 
       std::remove_reference_t<V> 
       > 
      > 
constexpr R operator()(Container&& c) 
{ 
    auto it = select(std::begin(c), std::end(c)); 
    return *make_forward_iterator<Container>(it); 
} 

Il y a probablement une façon moins prolixe pour exprimer tout cela.

+1

D'accord, mais cela ne montre pas comment transmettre la même valeur à plus de fonctions. –

+0

@thlst Okay mais votre question portait sur le morceau de code que vous avez posté. Si vous souhaitez poser une question sur un code différent, postez un code différent? – Barry

+3

@Barry: Sa question était assez claire; le code était juste * un exemple *, un morceau de code possible. Il ne pose pas de question sur le «travail de ce code». Il demande "est-il possible de le faire, avec un exemple". –

2

En général, oui, ceci est potentiellement dangereux.

La transmission d'un paramètre garantit que si la valeur reçue par le paramètre de référence universel est une valeur quelconque, elle continuera d'être une valeur de référence lorsqu'elle sera transmise. Si la valeur est finalement transmise à une fonction (telle qu'un constructeur de mouvement) que consomme la valeur en la déplaçant, son état interne n'est pas susceptible d'être valide pour une utilisation dans les appels suivants.

Si vous ne transférez pas le paramètre, il ne sera pas (en général) être éligibles aux opérations de déplacement, de sorte que vous serait à l'abri d'un tel comportement.

Dans votre cas, front et back (les fonctions libres et les fonctions membres) n'effectuent pas de déplacement sur le conteneur. Par conséquent, l'exemple spécifique que vous avez donné doit être sûr. Cependant, cela démontre également qu'il n'y a aucune raison de transférer le conteneur, car une valeur ne sera pas traitée différemment à partir d'une lvalue - ce qui est la seule raison de préserver la distinction en transmettant la valeur en premier lieu.

3

Vous probablement réaliser que vous ne voudriez pas std::move un objet passé à de multiples fonctions:

std::string s = "hello"; 
std::string hello1 = std::move(s); 
std::string hello2 = std::move(s); // hello2 != "hello" 

Le rôle de forward est tout simplement de restaurer un statut rvalue qu'un paramètre avait quand il a été passé à la fonction.

Nous pouvons démontrer rapidement qu'il est une mauvaise pratique en forward ing un paramètre deux fois à une fonction qui a un effet de mouvement:

#include <iostream> 
#include <string> 

struct S { 
    std::string name_ = "defaulted"; 
    S() = default; 
    S(const char* name) : name_(name) {} 
    S(S&& rhs) { std::swap(name_, rhs.name_); name_ += " moved"; } 
}; 

void fn(S s) 
{ 
    std::cout << "fn(" << s.name_ << ")\n"; 
} 

template<typename T> 
void fwd_test(T&& t) 
{ 
    fn(std::forward<T>(t)); 
    fn(std::forward<T>(t)); 
} 

int main() { 
    fwd_test(S("source")); 
} 

http://ideone.com/NRM8Ph

Si le renvoi était en sécurité, nous devrions voir fn(source moved) deux fois, mais à la place nous voyons:

fn(source moved) 
fn(defaulted moved)