2017-09-05 1 views
3

Disons que je l'utilise std::forward_as_tuple pour stocker les arguments d'un appel de fonction dans un tupleSimuler std :: avant avec std :: forward_as_tuple

auto args = std::forward_as_tuple(std::forward<Args>(args)...); 

Et puis je passe ce tuple par référence lvalue à une fonction qui veut pour appeler une fonction foo() avec certains des arguments dans args comme déterminé par un autre std::integer_sequence. Je le fais avec std::move() comme si

template <typename TupleArgs, std::size_t... Indices> 
decltype(auto) forward_to_foo(TupleArgs&& args, 
           std::index_sequence<Indices...>) { 
    return foo(std::get<Indices>(std::move(args))...); 
} 

Et cela fonctionnerait parce que la version qualifiée rvalue de std::get<std::tuple> retour std::tuple_element_t<Index, tuple<Types...>>&& qui est une transformation identitaire du-ness de référence du std::tuple_element_t<Index, tuple<Types...>> à cause de référence effondrement avec le &&.

Donc, si std::tuple_element_t<Index, tuple<Types...>> est évaluée à T& le type retourné serait T& && qui est juste T&. Raison similaire pour quand std::tuple_element_t<Index, tuple<Types...>> renvoie T&& et T

Ai-je raté quelque chose? Y a-t-il des cas où cela échouerait?

+0

Eh bien, 'TupleArgs & args' devrait être' TupleArgs args' ou 'TupleArgs && args' si vous vous en déplacez. Tout le reste rend très opaque au point d'appel que l'objet transmis est muté. – Yakk

+0

@Yakk droite, c'est quelque chose à considérer, c'est probablement mieux comme une référence de transfert. Le passer en valeur ne ferait que créer une copie des références sous-jacentes (ce qui est également bon je suppose) – Curious

+0

Et maintenant il devrait être un 'std :: forward ' au lieu d'un 'move' car' TupleArgs && 'ne doit pas être une rvalue ref. ;) (si c'est un rvalue ref, le foward fait la bonne chose de toute façon L'idée est de rendre l'exactitude du code comme une propriété locale comme raisonnable) – Yakk

Répondre

3
template <typename TupleArgs, std::size_t... Indices> 
decltype(auto) forward_to_foo(TupleArgs&& args, 
          std::index_sequence<Indices...>) { 
    return foo(std::get<Indices>(std::forward<TupleArgs>(args))...); 
} 

Ceci est l'implémentation correcte.

utilisation devrait ressembler à:

auto tuple_args = std::forward_as_tuple(std::forward<Args>(args)...); 
forward_to_foo(std::move(tuple_args), std::make_index_sequence<sizeof...(args)>{}); 

il y a quelques différences ici. D'abord, nous prenons en référence de renvoi, pas par référence de lvalue. Ceci permet à l'appelant de nous fournir des tuples rvalue (prvalue ou xvalue).

Deuxièmement, nous transmettre le tuple dans l'appel std::get. Cela signifie que nous transmettons seulement get une référence de référence si le tuple a été déplacé en nous.

Troisièmement, nous passons à forward_to_foo. Cela garantit que ce qui précède fait la bonne chose.

Maintenant, imaginez si nous voulions appeler foo deux fois.

auto tuple_args = std::forward_as_tuple(std::forward<Args>(args)...); 
auto indexes = std::make_index_sequence<sizeof...(args)>{}; 
forward_to_foo(tuple_args, indexes); 
forward_to_foo(std::move(tuple_args), indexes); 

nous n'avons pas toucher forward_to_foo du tout, et nous ne passons de l'un des args plus d'une fois.

Avec votre implémentation d'origine, tous les appels à forward_to_foo passent silencieusement de TupleArgs références ou valeurs sans aucune indication sur le site d'appel que nous sommes destructifs sur le premier paramètre.

Autre que ce détail, oui qui émule le transfert.


Moi-même je venais d'écrire un notstd::apply:

namespace notstd { 
    namespace details { 
    template <class F, class TupleArgs, std::size_t... Indices> 
    decltype(auto) apply(F&& f, TupleArgs&& args, 
         std::index_sequence<Indices...>) { 
     return std::forward<F>(f)(std::get<Indices>(std::forward<TupleArgs>(args))...); 
    } 
    } 
    template <class F, class TupleArgs> 
    decltype(auto) apply(F&& f, TupleArgs&& args) { 
    constexpr auto count = std::tuple_size< std::decay_t<TupleArgs> >{}; 
    return details::apply(
     std::forward<F>(f), 
     std::forward<TupleArgs>(args), 
     std::make_index_sequence<count>{} 
    ); 
    } 
} 

nous faisons:

auto tuple_args = std::forward_as_tuple(std::forward<Args>(args)...); 
auto call_foo = [](auto&&...args)->decltype(auto){ return foo(decltype(args)(args)...); }; 
return notstd::apply(call_foo, std::move(tuple_args)); 

qui se déplace le plus délicat dans notstd::apply, qui tente de faire correspondre la sémantique de std::apply , ce qui vous permet de le remplacer par un code plus standard.

+1

Mais quand le tuple est une lvalue, et un élément de tuple est 'T &&', ne pas rediriger le tuple vers 'get <>' aboutira à 'T &' qui pourrait créer une copie, etc. Ou voulez-vous dire que cela devrait être dû au fait de ne pas déplacer les arguments Cela aurait du sens idk – Curious

+3

Et cela est correct si le tuple est une lvalue Un tuple lvalue indique que l'appelant peut réutiliser le tuple plus tard (comme vous pouvez le voir dans mon appel 'foo' deux fois Dans ce cas, se déplacer d'arguments est * faux * dans le premier appel.Le deuxième appel, je fais la promesse de ne pas se soucier de l'état du tuple, et donc «déplacer» à partir de celui-ci – Yakk

+0

Juste par curiosité, pourquoi avez-vous présenté le lambda wrapper pour 'foo()' dans votre nouvel exemple? – Curious