Wie speichere ich verschiedene Vorlagenargumente?


87

Ist es möglich, ein Parameterpaket für eine spätere Verwendung zu speichern?

template <typename... T>
class Action {
private:        
    std::function<void(T...)> f;
    T... args;  // <--- something like this
public:
    Action(std::function<void(T...)> f, T... args) : f(f), args(args) {}
    void act(){
        f(args);  // <--- such that this will be possible
    }
}

Dann später:

void main(){
    Action<int,int> add([](int x, int y){std::cout << (x+y);}, 3, 4);

    //...

    add.act();
}

3
Ja; Speichern Sie ein Tupel und verwenden Sie eine Art Indexpaket, um den Anruf zu tätigen.
Kerrek SB

Antworten:


66

Um das zu erreichen, was Sie hier tun möchten, müssen Sie Ihre Vorlagenargumente in einem Tupel speichern:

std::tuple<Ts...> args;

Außerdem müssen Sie Ihren Konstruktor ein wenig ändern. Insbesondere das Initialisieren argsmit einem std::make_tupleund das Zulassen universeller Referenzen in Ihrer Parameterliste:

template <typename F, typename... Args>
Action(F&& func, Args&&... args)
    : f(std::forward<F>(func)),
      args(std::forward<Args>(args)...)
{}

Außerdem müssten Sie einen Sequenzgenerator wie folgt einrichten:

namespace helper
{
    template <int... Is>
    struct index {};

    template <int N, int... Is>
    struct gen_seq : gen_seq<N - 1, N - 1, Is...> {};

    template <int... Is>
    struct gen_seq<0, Is...> : index<Is...> {};
}

Und Sie können Ihre Methode so implementieren, dass Sie einen solchen Generator verwenden:

template <typename... Args, int... Is>
void func(std::tuple<Args...>& tup, helper::index<Is...>)
{
    f(std::get<Is>(tup)...);
}

template <typename... Args>
void func(std::tuple<Args...>& tup)
{
    func(tup, helper::gen_seq<sizeof...(Args)>{});
}

void act()
{
   func(args);
}

Und das ist es! Jetzt sollte Ihre Klasse so aussehen:

template <typename... Ts>
class Action
{
private:
    std::function<void (Ts...)> f;
    std::tuple<Ts...> args;
public:
    template <typename F, typename... Args>
    Action(F&& func, Args&&... args)
        : f(std::forward<F>(func)),
          args(std::forward<Args>(args)...)
    {}

    template <typename... Args, int... Is>
    void func(std::tuple<Args...>& tup, helper::index<Is...>)
    {
        f(std::get<Is>(tup)...);
    }

    template <typename... Args>
    void func(std::tuple<Args...>& tup)
    {
        func(tup, helper::gen_seq<sizeof...(Args)>{});
    }

    void act()
    {
        func(args);
    }
};

Hier ist dein vollständiges Programm auf Coliru.


Update: Hier ist eine Hilfsmethode, mit der die Angabe der Vorlagenargumente nicht erforderlich ist:

template <typename F, typename... Args>
Action<Args...> make_action(F&& f, Args&&... args)
{
    return Action<Args...>(std::forward<F>(f), std::forward<Args>(args)...);
}

int main()
{
    auto add = make_action([] (int a, int b) { std::cout << a + b; }, 2, 3);

    add.act();
}

Und wieder hier ist eine weitere Demo.


Wie würden Sie f (args) mit einem Tupel aufrufen? Können Sie das Tupel wieder in das Parameterpaket erweitern?
Eric B

1
Da Ts ... ein Klassenvorlagenparameter und kein Funktionsvorlagenparameter ist, definiert Ts && ... kein Paket universeller Referenzen, da für das Parameterpaket kein Typabzug erfolgt. @jogojapan zeigt den richtigen Weg, um sicherzustellen, dass Sie universelle Verweise an den Konstruktor übergeben können.
Masrtis

3
Achten Sie auf Referenzen und Objektlebensdauern! void print(const std::string&); std::string hello(); auto act = make_action(print, hello());ist nicht gut. Ich würde das Verhalten von bevorzugen std::bind, das eine Kopie jedes Arguments erstellt, es sei denn, Sie deaktivieren dies mit std::refoder std::cref.
Aschepler

1
Ich denke, @jogojapan hat eine viel präzisere und lesbarere Lösung.
Tim Kuipers

1
@Riddick Wechseln args(std::make_tuple(std::forward<Args>(args)...))zu args(std::forward<Args>(args)...). Übrigens habe ich das vor langer Zeit geschrieben und ich würde diesen Code nicht verwenden, um eine Funktion an einige Argumente zu binden. Ich würde nur verwenden std::invoke()oder std::apply()heutzutage.
0x499602D2

23

Sie können std::bind(f,args...)dies verwenden. Es wird ein bewegliches und möglicherweise kopierbares Objekt generiert, in dem eine Kopie des Funktionsobjekts und jedes der Argumente zur späteren Verwendung gespeichert wird:

#include <iostream>
#include <utility>
#include <functional>

template <typename... T>
class Action {
public:

  using bind_type = decltype(std::bind(std::declval<std::function<void(T...)>>(),std::declval<T>()...));

  template <typename... ConstrT>
  Action(std::function<void(T...)> f, ConstrT&&... args)
    : bind_(f,std::forward<ConstrT>(args)...)
  { }

  void act()
  { bind_(); }

private:
  bind_type bind_;
};

int main()
{
  Action<int,int> add([](int x, int y)
                      { std::cout << (x+y) << std::endl; },
                      3, 4);

  add.act();
  return 0;
}

Beachten Sie, dass dies std::bindeine Funktion ist und Sie das Ergebnis des Aufrufs als Datenelement speichern müssen. Der Datentyp dieses Ergebnisses ist nicht leicht vorherzusagen (der Standard spezifiziert ihn nicht einmal genau), daher verwende ich eine Kombination aus decltypeund std::declval, um diesen Datentyp zur Kompilierungszeit zu berechnen. Siehe die Definition von Action::bind_typeoben.

Beachten Sie auch, wie ich universelle Referenzen im Vorlagenkonstruktor verwendet habe. Dies stellt sicher, dass Sie Argumente übergeben können, die nicht T...genau mit den Klassenvorlagenparametern übereinstimmen (z. B. können Sie rWert-Verweise auf einige der Parameter verwenden und diese unverändert Tan den bindAufruf weiterleiten .)

Schlussbemerkung: Wenn Sie Argumente als Referenzen speichern möchten (damit die von Ihnen übergebene Funktion sie ändern und nicht nur verwenden kann), müssen Sie std::refsie in Referenzobjekte einschließen. Durch einfaches Übergeben von a T &wird eine Kopie des Werts erstellt, keine Referenz.

Betriebscode auf Coliru


Ist es nicht gefährlich, Werte zu binden? Würden diese nicht ungültig werden, wenn addsie in einem anderen Bereich definiert sind als wo act()? Sollte der Konstruktor nicht ConstrT&... argseher bekommen als ConstrT&&... args?
Tim Kuipers

1
@ Angelorf Entschuldigung für meine späte Antwort. Du meinst rWerte im Aufruf an bind()? Da bind()garantiert Kopien erstellt werden (oder in frisch erstellte Objekte verschoben werden), kann es meines Erachtens kein Problem geben.
Jogojapan

@jogojapan Kurzer Hinweis, MSVC17 erfordert, dass die Funktion im Konstruktor auch an bind_ weitergeleitet wird (dh bind_ (std :: forward <std :: function <void (T ...) >> (f), std :: forward <) ConstrT> (args) ...))
Überstrahlt

1
Im Initialisierer bind_(f, std::forward<ConstrT>(args)...)ist das Verhalten gemäß dem Standard undefiniert, da dieser Konstruktor implementierungsdefiniert ist. bind_typewird als kopierbar und / oder verschiebbar konstruierbar angegeben, bind_{std::bind(f, std::forward<ConstrT>(args)...)}sollte also weiterhin funktionieren.
Joshtch

3

Ich denke du hast ein XY Problem. Warum sollten Sie sich die Mühe machen, das Parameterpaket zu speichern, wenn Sie nur ein Lambda auf der Call-Site verwenden könnten? dh

#include <functional>
#include <iostream>

typedef std::function<void()> Action;

void callback(int n, const char* s) {
    std::cout << s << ": " << n << '\n';
}

int main() {
    Action a{[]{callback(13, "foo");}};
    a();
}

Denn in meiner Anwendung hat eine Aktion tatsächlich 3 verschiedene Funktionen, die alle miteinander verbunden sind, und ich möchte lieber, dass Klassen, die sie enthalten, 1 Aktion und nicht 3 std :: function enthalten
Eric B

3

Diese Frage war aus C ++ 11 Tage. Aber für diejenigen, die es jetzt in den Suchergebnissen finden, einige Updates:

Ein std::tupleMitglied ist immer noch die einfache Möglichkeit, Argumente allgemein zu speichern. (Eine std::bindLösung ähnlich der von @ jogojapan funktioniert auch, wenn Sie nur eine bestimmte Funktion aufrufen möchten, aber nicht, wenn Sie auf andere Weise auf die Argumente zugreifen oder die Argumente an mehr als eine Funktion übergeben möchten usw.)

In C ++ 14 und höher std::make_index_sequence<N>oderstd::index_sequence_for<Pack...> kann das helper::gen_seq<N>in der Lösung von 0x499602D2 gezeigte Tool ersetzen :

#include <utility>

template <typename... Ts>
class Action
{
    // ...
    template <typename... Args, std::size_t... Is>
    void func(std::tuple<Args...>& tup, std::index_sequence<Is...>)
    {
        f(std::get<Is>(tup)...);
    }

    template <typename... Args>
    void func(std::tuple<Args...>& tup)
    {
        func(tup, std::index_sequence_for<Args...>{});
    }
    // ...
};

In C ++ 17 und höher std::applykann das Auspacken des Tupels verwendet werden:

template <typename... Ts>
class Action
{
    // ...
    void act() {
        std::apply(f, args);
    }
};

Hier ist ein vollständiges C ++ 17-Programm, das die vereinfachte Implementierung zeigt. Ich habe auch aktualisiert make_action, um Referenztypen in der zu vermeiden tuple, was für rvalue-Argumente immer schlecht und für lvalue-Argumente ziemlich riskant war.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.