web-dev-qa-db-fra.com

Capture d'une variable parfaitement transmise dans lambda

template<typename T> void doSomething(T&& mStuff)
{
    auto lambda([&mStuff]{ doStuff(std::forward<T>(mStuff)); });
    lambda();
}

Est-il correct de capturer la variable mStuff parfaitement transmise avec la &mStuff syntaxe?

Ou existe-t-il une syntaxe de capture spécifique pour les variables parfaitement transmises?

EDIT: Et si la variable parfaitement transmise est un pack de paramètres?

47
Vittorio Romeo

Est-il correct de capturer la variable mStuff parfaitement transmise avec la syntaxe & mStuff?

Oui, en supposant que vous n'utilisez pas ce lambda en dehors de doSomething. Votre code capture mStuff par référence et le transmettra correctement à l'intérieur du lambda.

Pour que mStuff soit un pack de paramètres, il suffit d'utiliser une capture simple avec une extension de pack:

template <typename... T> void doSomething(T&&... mStuff)
{
    auto lambda = [&mStuff...]{ doStuff(std::forward<T>(mStuff)...); };
}

Le lambda capture chaque élément de mStuff par référence. L'objet de fermeture enregistre une référence lvalue pour chaque argument, quelle que soit sa catégorie de valeur. Une transmission parfaite fonctionne toujours; En fait, il n'y a même pas de différence car les références rvalue nommées seraient de toute façon des lvalues.

24
Columbo

Pour rendre le lambda valide en dehors de la portée où il est créé, vous avez besoin d'une classe wrapper qui gère les valeurs lvalue et rvalues ​​différemment, c'est-à-dire qui garde une référence à une valeur l, mais fait une copie (en déplaçant) une valeur r.

Fichier d'en-tête capture.h:

#pragma once

#include <type_traits>
#include <utility>

template < typename T >
class capture_wrapper
{
   static_assert(not std::is_rvalue_reference<T>{},"");
   std::remove_const_t<T> mutable val_;
public:
   constexpr explicit capture_wrapper(T&& v)
      noexcept(std::is_nothrow_move_constructible<std::remove_const_t<T>>{})
   :val_(std::move(v)){}
   constexpr T&& get() const noexcept { return std::move(val_); }
};

template < typename T >
class capture_wrapper<T&>
{
   T& ref_;
public:
   constexpr explicit capture_wrapper(T& r) noexcept : ref_(r){}
   constexpr T& get() const noexcept { return ref_; }
};

template < typename T >
constexpr typename std::enable_if<
   std::is_lvalue_reference<T>{},
   capture_wrapper<T>
>::type
capture(std::remove_reference_t<T>& t) noexcept
{
   return capture_wrapper<T>(t);
}

template < typename T >
constexpr typename std::enable_if<
   std::is_rvalue_reference<T&&>{},
   capture_wrapper<std::remove_reference_t<T>>
>::type
capture(std::remove_reference_t<T>&& t)
   noexcept(std::is_nothrow_constructible<capture_wrapper<std::remove_reference_t<T>>,T&&>{})
{
   return capture_wrapper<std::remove_reference_t<T>>(std::move(t));
}

template < typename T >
constexpr typename std::enable_if<
   std::is_rvalue_reference<T&&>{},
   capture_wrapper<std::remove_reference_t<T>>
>::type
capture(std::remove_reference_t<T>& t)
   noexcept(std::is_nothrow_constructible<capture_wrapper<std::remove_reference_t<T>>,T&&>{})
{
   return capture_wrapper<std::remove_reference_t<T>>(std::move(t));
}

Exemple/code de test qui montre que cela fonctionne. Notez que l'exemple "bar" montre comment utiliser std::Tuple<...> pour contourner le manque d'expansion du pack dans l'initialiseur de capture lambda, utile pour la capture variadique.

#include <cassert>
#include <Tuple>
#include "capture.h"

template < typename T >
auto foo(T&& t)
{
   return [t = capture<T>(t)]()->decltype(auto)
   {
      auto&& x = t.get();
      return std::forward<decltype(x)>(x);
      // or simply, return t.get();
   };
}

template < std::size_t... I, typename... T >
auto bar_impl(std::index_sequence<I...>, T&&... t)
{
   static_assert(std::is_same<std::index_sequence<I...>,std::index_sequence_for<T...>>{},"");
   return [t = std::make_Tuple(capture<T>(t)...)]()
   {
      return std::forward_as_Tuple(std::get<I>(t).get()...);
   };
}
template < typename... T >
auto bar(T&&... t)
{
   return bar_impl(std::index_sequence_for<T...>{}, std::forward<T>(t)...);
}

int main()
{
   static_assert(std::is_same<decltype(foo(0)()),int&&>{}, "");
   assert(foo(0)() == 0);

   auto i = 0;
   static_assert(std::is_same<decltype(foo(i)()),int&>{}, "");
   assert(&foo(i)() == &i);

   const auto j = 0;
   static_assert(std::is_same<decltype(foo(j)()),const int&>{}, "");
   assert(&foo(j)() == &j);

   const auto&& k = 0;
   static_assert(std::is_same<decltype(foo(std::move(k))()),const int&&>{}, "");
   assert(foo(std::move(k))() == k);

   auto t = bar(0,i,j,std::move(k))();
   static_assert(std::is_same<decltype(t),std::Tuple<int&&,int&,const int&,const int&&>>{}, "");
   assert(std::get<0>(t) == 0);
   assert(&std::get<1>(t) == &i);
   assert(&std::get<2>(t) == &j);
   assert(std::get<3>(t) == k and &std::get<3>(t) != &k);

}
6
Hui

TTBOMK, pour C++ 14, je pense que les solutions ci-dessus pour la gestion à vie peuvent être simplifiées pour:

template <typename T> capture { T value; }

template <typename T>
auto capture_example(T&& value) {
  capture<T> cap{std::forward<T>(value)};
  return [cap = std::move(cap)]() { /* use cap.value *; };
};

ou plus anonyme:

template <typename T>
auto capture_example(T&& value) {
  struct { T value; } cap{std::forward<T>(value)};
  return [cap = std::move(cap)]() { /* use cap.value *; };
};

Je l'ai utilisé ici (certes, ce bloc de code particulier est plutôt inutile: P)

https://github.com/EricCousineau-TRI/repro/blob/3fda1e0/cpp/generator.cc#L161-L176

4
Eric Cousineau

Oui, vous pouvez faire une capture parfaite, mais pas directement. Vous devrez envelopper le type dans une autre classe:

#define REQUIRES(...) class=std::enable_if_t<(__VA_ARGS__)>

template<class T>
struct wrapper
{
    T value;
    template<class X, REQUIRES(std::is_convertible<T, X>())>
    wrapper(X&& x) : value(std::forward<X>(x))
    {}

    T get() const
    {
        return std::move(value);
    }
};

template<class T>
auto make_wrapper(T&& x)
{
    return wrapper<T>(std::forward<T>(x));
}

Ensuite, passez-les en tant que paramètres à un lambda qui renvoie un lambda imbriqué qui capture les paramètres par valeur:

template<class... Ts>
auto do_something(Ts&&... xs)
{
    auto lambda = [](auto... ws)
    {
        return [=]()
        {
            // Use `.get()` to unwrap the value
            some_other_function(ws.get()...);
        };
    }(make_wrapper(std::forward<Ts>(xs)...));

    lambda();
}
4
Paul Fultz II

Voici une solution pour C++ 17 qui utilise guides de déduction pour vous faciliter la tâche. J'élabore sur Vittorio Romeo (l'OP) blog post , où il fournit une solution à sa propre question.

std::Tuple Peut être utilisé pour encapsuler les variables parfaitement transmises, en faisant une copie ou en conservant une référence de chacune d'elles sur une base par variable, selon les besoins. Le Tuple lui-même est capturé par le lambda.

Pour le rendre plus facile et plus propre, je vais créer un nouveau type dérivé de std::Tuple, Afin de fournir une construction guidée (qui nous permettra d'éviter les fonctions std::forward Et decltype() passe-partout) et des accesseurs de type pointeur dans le cas où il n'y a qu'une seule variable à capturer.

// This is the generic case
template <typename... T>
struct forwarder: public std::Tuple<T...> {
    using std::Tuple<T...>::Tuple;        
};

// This is the case when just one variable is being captured.
template <typename T>
struct forwarder<T>: public std::Tuple<T> {
    using std::Tuple<T>::Tuple;

    // Pointer-like accessors
    auto &operator *() {
        return std::get<0>(*this);
    }

    const auto &operator *() const {
        return std::get<0>(*this);
    }

    auto *operator ->() {
        return &std::get<0>(*this);
    }

    const auto *operator ->() const {
        return &std::get<0>(*this);
    }
};

// std::Tuple_size needs to be specialized for our type, 
// so that std::apply can be used.
namespace std {
    template <typename... T>
    struct Tuple_size<forwarder<T...>>: Tuple_size<Tuple<T...>> {};
}

// The below two functions declarations are used by the deduction guide
// to determine whether to copy or reference the variable
template <typename T>
T forwarder_type(const T&);

template <typename T>
T& forwarder_type(T&);

// Here comes the deduction guide
template <typename... T>
forwarder(T&&... t) -> forwarder<decltype(forwarder_type(std::forward<T>(t)))...>;

Et puis on peut l'utiliser comme suit.

La version variadique:

// Increment each parameter by 1 at each invocation and print it.
// Rvalues will be copied, Lvalues will be passed as references.
auto variadic_incrementer = [](auto&&... a)
{
    return [a = forwarder(a...)]() mutable 
    { 
        std::apply([](auto &&... args) {
            (++args._value,...);
            ((std::cout << "variadic_incrementer: " << args._value << "\n"),...);
        }, a);
    };
};

La version non variadique:

// Increment the parameter by 1 at each invocation and print it.
// Rvalues will be copied, Lvalues will be passed as references.
auto single_incrementer = [](auto&& a)
{
    return [a = forwarder(a)]() mutable 
    { 
        ++a->_value;
        std::cout << "single_incrementer: " << a->_value << "\n";
    };
};
1
Fabio A.