Wie erstelle ich eine std::function aus einem Move-Capturing-Lambda-Ausdruck?

Lesezeit: 9 Minuten

Wie erstelle ich eine stdfunction aus einem Move Capturing Lambda Ausdruck
seertaak

Ich versuche, eine zu erstellen std::function aus einem bewegungserfassenden Lambda-Ausdruck. Beachten Sie, dass ich problemlos einen bewegungserfassenden Lambda-Ausdruck erstellen kann; es ist nur, wenn ich versuche, es in ein zu wickeln std::function dass ich einen Fehler bekomme.

Zum Beispiel:

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);

Ich werde erklären, warum ich so etwas schreiben möchte. Ich habe eine UI-Bibliothek geschrieben, die es dem Benutzer ähnlich wie jQuery oder JavaFX ermöglicht, Maus-/Tastaturereignisse im Vorbeigehen zu verarbeiten std::functions zu Methoden mit Namen wie on_mouse_down(), on_mouse_drag(), push_undo_action()etc.

Offensichtlich die std::function Ich möchte übergeben, sollte idealerweise einen Move-Capturing-Lambda-Ausdruck verwenden, andernfalls muss ich auf die hässliche Redewendung „Release/Acquire-in-Lambda“ zurückgreifen, die ich verwendet habe, als C++11 der Standard war:

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

Beachten Sie das Aufrufen baz zweimal wäre ein Fehler im obigen Code. In meinem Code wird diese Closure jedoch garantiert genau einmal aufgerufen.

Übrigens, in meinem echten Code übergebe ich keine std::unique_ptr<int>aber etwas Interessanteres.

Schließlich verwende ich Xcode6-Beta4, das die folgende Version von Clang verwendet:

Apple LLVM version 5.1 (clang-503.0.40) (based on LLVM 3.4svn)
Target: x86_64-apple-darwin13.3.0
Thread model: posix

  • Du kannst nicht. std::function erfordert das Funktionsobjekt be CopyConstructible.

    – TC

    21. August 2014 um 8:23 Uhr

  • Dies ist sehr ähnlich zu stackoverflow.com/questions/25330716/… Warum auch nicht einfach Vorlagen für die Funktionen verwenden, anstatt std::function Typ löschen? Verwenden std::function als universeller Funktionstyp ist keine gute Idee.

    – Manu343726

    21. August 2014 um 8:27 Uhr


  • Nun, die Idee war, zu vermeiden, dass die Kompilierungszeiten zu lang werden, kombiniert mit der Tatsache, dass die Leistungseinbuße für die Verwendung von std::function ist im Kontext von UI-Callbacks akzeptabel. (Vielleicht voreilige Optimierung!)

    – seertaak

    21. August 2014 um 8:31 Uhr


  • Verwandte Frage: Nur-Verschieben-Version von std::function

    – maxschlepzig

    14. Oktober 2018 um 18:54 Uhr

  • Die jetzt im Standard enthaltene Range-Bibliothek behebt dieses Problem (nur hier aus Gründen der Abwärtskompatibilität) mit einem semiregulären Wrapper: eel.is/c++draft/range.semi.wrap

    – Oliv

    13. Dezember 2018 um 12:03 Uhr

template<class F> function(F f);

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

Erfordert: F muss CopyConstructible. f muss Callable für Argumenttypen ArgTypes und Rückgabetyp R. Der Kopierkonstruktor und -destruktor von A dürfen keine Ausnahmen auslösen.

§20.9.11.2.1 [func.wrap.func.con]

Beachten Sie, dass operator = wird in Bezug auf diesen Konstruktor und definiert swapdaher gelten die gleichen Einschränkungen:

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

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

§20.9.11.2.1 [func.wrap.func.con]

Um Ihre Frage zu beantworten: Ja, es ist möglich, a zu konstruieren std::function von einem bewegungserfassenden Lambda (da dies nur angibt, wie das Lambda erfasst), aber es ist nicht möglich, eine zu konstruieren std::function von einem Nur-Verschieben-Typ (z. B. ein bewegungserfassendes Lambda, das etwas bewegungserfasst, das nicht kopierkonstruierbar ist).

  • Ich verstehe den Unterschied zwischen dem ersten und dem zweiten Satz in Ihrem letzten Satz (“es ist möglich, … zu konstruieren” und “aber es ist nicht möglich …”) nicht. Können Sie vielleicht ein Beispiel nennen?

    – seertaak

    21. August 2014 um 8:28 Uhr


  • @seertaak: Angenommen, Sie haben ein Lambda, das a erfasst std::vector durch Umzug. Die std::vector wird in das Lambda verschoben, aber std::vector ist CopyConstructible, und daher ist das Lambda kopierbar, obwohl es durch Verschieben erfasst wurde. Daher kann dieses Lambda, wenn alles andere gleich ist, verwendet werden, um a zu konstruieren std::function. Das heißt, wie Sie etwas erfassen, ist nicht wirklich relevant, was relevant ist, sind die Eigenschaften, die diese Objekte dem einmal erfassten Lambda geben.

    – Robert Allan Hennigan Leahy

    21. August 2014 um 8:31 Uhr


  • Warten Sie: Ints sind kopierkonstruierbar, genau wie std::vector. Warum sollte es dann nicht mit ints funktionieren?

    – seertaak

    21. August 2014 um 8:33 Uhr

  • @seertaak Sie erfassen std::unique_ptrnicht ein int. Versuchen Sie es durch zu ersetzen std::shared_ptr die kopierkonstruierbar ist.

    – Ivan Aksamentov – Tropfen

    21. August 2014 um 8:34 Uhr


  • @seertaak Siehe auch eine Erklärung von Warum in aller Welt std::function erfordert, dass Funktor kopierkonstruierbar ist und hier eine Problemumgehung über einen Adapter.

    – Ivan Aksamentov – Tropfen

    21. August 2014 um 8:45 Uhr


1646868611 453 Wie erstelle ich eine stdfunction aus einem Move Capturing Lambda Ausdruck
Yakk – Adam Nevraumont

Als std::function<?> den Kopierkonstruktor des gespeicherten aufrufbaren Objekts löschen muss, können Sie ihn nicht aus einem Nur-Verschieben-Typ erstellen. Ihr Lambda ist, da es einen Move-Only-Typ nach Wert erfasst, ein Move-Only-Typ. Also … Sie können Ihr Problem nicht lösen. std::function kann Ihr Lambda nicht speichern.

Zumindest nicht direkt.

Das ist C++, wir umgehen das Problem einfach.

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) };
}

Jetzt, da dies erledigt ist, können wir Ihr Problem lösen.

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

Die Semantik von a shared_function unterscheidet sich geringfügig von anderen Funktionen, da eine Kopie davon den gleichen Status hat (auch wenn sie in eine std::function) als Original.

Wir können auch eine Move-only-Feuer-Einmal-Funktion schreiben:

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;
};

die sogar nicht verschiebbare Typen über die unterstützt emplace_as<T> Schild.

Live-Beispiel.

Beachten Sie, dass Sie bewerten müssen () in einem rvalue-Kontext (d. h. nach a std::move), als stille destruktive () schien unhöflich.

Diese Implementierung verwendet kein SBO, denn wenn dies der Fall wäre, würde es erfordern, dass der gespeicherte Typ verschiebbar ist, und es wäre (für mich) mehr Arbeit zu booten.

  • Warum wäre es ein Problem, eine fire_once-Funktion zweimal aufzurufen? Solange es das aufrufbare enthält, sollte es in Ordnung sein, das Objekt aufzurufen, oder?

    – Benutzer877329

    29. April 2020 um 19:33 Uhr

  • @user8 [x=std::optional<T>(x)]()mutable{ auto r = std::move(*x); x=std::nullopt; return r; } ist ein Spielzeugbeispiel; eine Funktion, die nach dem Aufrufen für einen erneuten Aufruf ungültig wird. Proxy-Move-Konstruktoren im Allgemeinen und in jedem Fall, in dem das Kopieren mehr kostet als zu verbrauchen.

    – Yakk – Adam Nevraumont

    1. Mai 2020 um 1:48 Uhr


Hier ist eine einfachere Lösung:

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

   auto ppi = std::make_shared<std::unique_ptr<int>>(std::move(pi));

   std::function<void()> bar = [ppi] {
        **ppi = 5;
        std::cout << **ppi << std::endl;
   };

Live-Beispiel hier

  • Ein ähnlicher Trick wird in erwähnt dieses Gespräch.

    – Evg

    12. November 2019 um 20:58 Uhr

  • Danke @Taylor, das war ein sehr kniffliges Rätsel

    – Adamsky

    2. Juli 2020 um 15:44 Uhr

  • @Evg Danke, dass du diesen Vortrag geteilt hast. Von diesem Gespräch weiß ich es jetzt folly::Function könnte in diesem Szenario verwendet werden. Das hat viel Zeit gespart.

    – Christoph

    20. September 2020 um 17:33 Uhr

985000cookie-checkWie erstelle ich eine std::function aus einem Move-Capturing-Lambda-Ausdruck?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy