¿Cómo crear una función std:: a partir de una expresión lambda que captura movimientos?


Estoy intentando crear un std::function a partir de una expresión lambda que captura movimientos. Tenga en cuenta que puedo crear una expresión lambda de captura de movimiento sin problemas; es solo cuando intento envolverla en un std::function que obtengo un error.

Por ejemplo:

auto pi = std::make_unique<int>(0);

// no problems here!
auto foo = [q = std::move(pi)] {
    *q = 5;
    std::cout << *q << std::endl;
};

// All of the attempts below yield:
// "Call to implicitly-deleted copy constructor of '<lambda...."

std::function<void()> bar = foo;
std::function<void()> bar{foo};
std::function<void()> bar{std::move(foo)};
std::function<void()> bar = std::move(foo);
std::function<void()> bar{std::forward<std::function<void()>>(foo)};
std::function<void()> bar = std::forward<std::function<void()>>(foo);

Voy a explicar por qué quiero escribir algo como esto. He escrito una biblioteca de interfaz de usuario que, similar a jQuery o JavaFX, permite al usuario manejar eventos de ratón / teclado pasando std::functions a métodos con nombres como on_mouse_down(), on_mouse_drag(), push_undo_action(), sucesivamente.

Obviamente, el std::function que quiero pasar idealmente debería usar una expresión lambda que capture movimientos, de lo contrario necesito recurrir al feo modismo" release/acquire-in-lambda " que estaba usando cuando C++11 era el estándar:

std::function<void()> baz = [q = pi.release()] {
    std::unique_ptr<int> p{q};
    *p = 5;
    std::cout << *q << std::endl;
};

Tenga en cuenta que llamar a baz dos veces sería un error en el código anterior. Sin embargo, en mi código, se garantiza que este cierre se llamará exactamente una vez.

Por cierto, en mi código real, no estoy pasando un std::unique_ptr<int>, sino algo más Interesante....

Finalmente, estoy usando Xcode6-Beta4 que usa la siguiente versión de clang:

Apple LLVM version 5.1 (clang-503.0.40) (based on LLVM 3.4svn)
Target: x86_64-apple-darwin13.3.0
Thread model: posix
Author: Walter, 2014-08-21

2 answers

template<class F> function(F f);

template <class F, class A> function(allocator_arg_t, const A& a, F f);

Requiere: F será CopyConstructible. f será Callable para los tipos de argumento ArgTypes y el tipo de retorno R. El constructor de copia y el destructor de A no lanzarán excepciones.

§20.9.11.2.1 [func.envolver.func.con]

Tenga en cuenta que operator = se define en términos de este constructor y swap, por lo que se aplican las mismas restricciones:

template<class F> function& operator=(F&& f);

Efectos: function(std::forward<F>(f)).swap(*this);

§20.9.11.2.1 [func.envolver.func.con]

Así que para responder a su pregunta: Sí, es posible construir un std::function a partir de un lambda de captura de movimiento (ya que esto solo especifica cómo captura el lambda), pero es no posible construir un std::function a partir de un tipo de solo movimiento (por ejemplo, un lambda de captura de movimiento que captura algo que no se puede copiar).

 24
Author: Robert Allan Hennigan Leahy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-08-21 08:22:12

Como std::function<?> tiene que escribir-borrar el constructor de copia del objeto invocable almacenado, no puede construirlo a partir de un tipo de solo movimiento. Su lambda, ya que captura un tipo de solo movimiento por valor, es un tipo de solo movimiento. Tan... no puedes resolver tu problema. std::function no puede almacenar su lambda.

Al menos no directamente.

Esto es C++, simplemente enrutamos el problema.

template<class F>
struct shared_function {
  std::shared_ptr<F> f;
  shared_function() = delete; // = default works, but I don't use it
  shared_function(F&& f_):f(std::make_shared<F>(std::move(f_))){}
  shared_function(shared_function const&)=default;
  shared_function(shared_function&&)=default;
  shared_function& operator=(shared_function const&)=default;
  shared_function& operator=(shared_function&&)=default;
  template<class...As>
  auto operator()(As&&...as) const {
    return (*f)(std::forward<As>(as)...);
  }
};
template<class F>
shared_function< std::decay_t<F> > make_shared_function( F&& f ) {
  return { std::forward<F>(f) };
}

Ahora que lo anterior se hace, podemos resolver su problema.

auto pi = std::make_unique<int>(0);

auto foo = [q = std::move(pi)] {
  *q = 5;
  std::cout << *q << std::endl;
};

std::function< void() > test = make_shared_function( std::move(foo) );
test(); // prints 5

La semántica de a shared_function es ligeramente diferente a otras funciones, ya que una copia de ella comparte el mismo estado (incluso cuando se convierte en a std::function) que el original.

También podemos escribir una función move-only fire-once:

template<class Sig>
struct fire_once;

template<class T>
struct emplace_as {};

template<class R, class...Args>
struct fire_once<R(Args...)> {
  // can be default ctored and moved:
  fire_once() = default;
  fire_once(fire_once&&)=default;
  fire_once& operator=(fire_once&&)=default;

  // implicitly create from a type that can be compatibly invoked
  // and isn't a fire_once itself
  template<class F,
    std::enable_if_t<!std::is_same<std::decay_t<F>, fire_once>{}, int> =0,
    std::enable_if_t<
      std::is_convertible<std::result_of_t<std::decay_t<F>&(Args...)>, R>{}
      || std::is_same<R, void>{},
      int
    > =0
  >
  fire_once( F&& f ):
    fire_once( emplace_as<std::decay_t<F>>{}, std::forward<F>(f) )
  {}
  // emplacement construct using the emplace_as tag type:
  template<class F, class...FArgs>
  fire_once( emplace_as<F>, FArgs&&...fargs ) {
    rebind<F>(std::forward<FArgs>(fargs)...);
  }
  // invoke in the case where R is not void:
  template<class R2=R,
    std::enable_if_t<!std::is_same<R2, void>{}, int> = 0
  >
  R2 operator()(Args...args)&&{
    try {
      R2 ret = invoke( ptr.get(), std::forward<Args>(args)... );
      clear();
      return ret;
    } catch(...) {
      clear();
      throw;
    }
  }
  // invoke in the case where R is void:
  template<class R2=R,
    std::enable_if_t<std::is_same<R2, void>{}, int> = 0
  >
  R2 operator()(Args...args)&&{
    try {
      invoke( ptr.get(), std::forward<Args>(args)... );
      clear();
    } catch(...) {
      clear();
      throw;
    }
  }

  // empty the fire_once:
  void clear() {
    invoke = nullptr;
    ptr.reset();
  }

  // test if it is non-empty:
  explicit operator bool()const{return (bool)ptr;}

  // change what the fire_once contains:
  template<class F, class...FArgs>
  void rebind( FArgs&&... fargs ) {
    clear();
    auto pf = std::make_unique<F>(std::forward<FArgs>(fargs)...);
    invoke = +[](void* pf, Args...args)->R {
      return (*(F*)pf)(std::forward<Args>(args)...);
    };
    ptr = {
      pf.release(),
      [](void* pf){
        delete (F*)(pf);
      }
    };
  }
private:
  // storage.  A unique pointer with deleter
  // and an invoker function pointer:
  std::unique_ptr<void, void(*)(void*)> ptr{nullptr, +[](void*){}};
  void(*invoke)(void*, Args...) = nullptr;
};

Que soporta incluso tipos no móviles a través de la etiqueta emplace_as<T>.

Ejemplo en vivo .

Tenga en cuenta que tiene que evaluar () en un contexto rvalue (es decir, después de un std::move), como un destructivo silencioso () parecía grosero.

Esta implementación no utiliza SBO, porque si lo hiciera exigiría que el tipo almacenado fuera movible, y sería más trabajo (para mí) arrancar.

 20
Author: Yakk - Adam Nevraumont,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-26 17:05:47