Ein Tupel „entpacken“, um einen passenden Funktionszeiger aufzurufen

254

Ich versuche, in einer std::tuplevariierenden Anzahl von Werten zu speichern , die später als Argumente für einen Aufruf eines Funktionszeigers verwendet werden, der den gespeicherten Typen entspricht.

Ich habe ein vereinfachtes Beispiel erstellt, das das Problem zeigt, das ich nur schwer lösen kann:

#include <iostream>
#include <tuple>

void f(int a, double b, void* c) {
  std::cout << a << ":" << b << ":" << c << std::endl;
}

template <typename ...Args>
struct save_it_for_later {
  std::tuple<Args...> params;
  void (*func)(Args...);

  void delayed_dispatch() {
     // How can I "unpack" params to call func?
     func(std::get<0>(params), std::get<1>(params), std::get<2>(params));
     // But I *really* don't want to write 20 versions of dispatch so I'd rather 
     // write something like:
     func(params...); // Not legal
  }
};

int main() {
  int a=666;
  double b = -1.234;
  void *c = NULL;

  save_it_for_later<int,double,void*> saved = {
                                 std::tuple<int,double,void*>(a,b,c), f};
  saved.delayed_dispatch();
}

Normalerweise std::tuplewürde ich bei Problemen mit oder mit variablen Vorlagen eine andere Vorlage schreiben template <typename Head, typename ...Tail>, um alle Typen nacheinander rekursiv auszuwerten, aber ich sehe keine Möglichkeit, dies für den Versand eines Funktionsaufrufs zu tun.

Die eigentliche Motivation dafür ist etwas komplexer und meistens nur eine Lernübung. Sie können davon ausgehen, dass ich das Tupel vertraglich von einer anderen Schnittstelle erhalten habe, es kann also nicht geändert werden, aber der Wunsch, es in einen Funktionsaufruf zu entpacken, liegt bei mir. Dies std::bindschließt aus, dass das zugrunde liegende Problem auf billige Weise umgangen wird.

Was ist eine saubere Methode, um den Anruf mit dem zu versenden std::tuple, oder eine alternative bessere Methode, um das gleiche Nettoergebnis zu erzielen, indem einige Werte und ein Funktionszeiger bis zu einem beliebigen zukünftigen Punkt gespeichert / weitergeleitet werden?

Flexo
quelle
5
Warum kannst du nicht einfach auto saved = std::bind(f, a, b, c);... dann später einfach anrufen saved()?
Charles Salvia
Nicht immer meine Schnittstelle zu steuern. Ich erhalte vertraglich ein Tupel von jemand anderem und möchte später etwas damit anfangen.
Flexo

Antworten:

275

Sie müssen ein Parameterpaket mit Zahlen erstellen und diese entpacken

template<int ...>
struct seq { };

template<int N, int ...S>
struct gens : gens<N-1, N-1, S...> { };

template<int ...S>
struct gens<0, S...> {
  typedef seq<S...> type;
};


// ...
  void delayed_dispatch() {
     callFunc(typename gens<sizeof...(Args)>::type());
  }

  template<int ...S>
  void callFunc(seq<S...>) {
     func(std::get<S>(params) ...);
  }
// ...
Johannes Schaub - litb
quelle
4
Wow, ich wusste nicht, dass der Auspacker so benutzt werden kann, das ist schön!
Luc Touraille
5
Johannes, mir ist klar, dass es mehr als zwei Jahre her ist, seit Sie dies veröffentlicht haben, aber das einzige, mit dem ich zu kämpfen habe, ist die struct gensgenerische Definition (diejenige, die von einer erweiterten Ableitung desselben erbt ). Ich sehe, dass es schließlich die Spezialisierung mit 0 trifft. Wenn die Stimmung zu Ihnen passt und Sie die freien Zyklen haben, wenn Sie das erweitern können und wie es dafür verwendet wird, wäre ich auf ewig dankbar. Und ich wünschte, ich könnte dies hundertmal abstimmen. Ich hatte mehr Spaß daran, mit Tangenten aus diesem Code zu spielen. Vielen Dank.
WhozCraig
22
@ WhozCraig: Es wird ein Typ generiert seq<0, 1, .., N-1>. Wie es funktioniert : gens<5>: gens<4, 4>: gens<3, 3, 4>: gens<2, 2, 3, 4> : gens<1, 1, 2, 3, 4> : gens<0, 0, 1, 2, 3, 4>. Der letzte Typ ist spezialisiert und erstellt seq<0, 1, 2, 3, 4>. Ziemlich kluger Trick.
Mindvirus
2
@ NirFriedman: Sicher, ersetzen Sie einfach die nicht spezialisierte Version von gensdurch:template <int N, int... S> struct gens { typedef typename gens<N-1, N-1, S...>::type type; };
marton78
11
Es lohnt sich, Walters Antwort und Kommentare dazu zu wiederholen: Die Leute müssen ihre eigenen Räder nicht mehr erfinden. Das Generieren einer Sequenz war so häufig, dass sie in C ++ 14 als std::integer_sequence<T, N>und deren Spezialisierung für std::size_t, std::index_sequence<N>- plus die zugehörigen Hilfsfunktionen std::make_in(teger|dex)_sequence<>()und standardisiert wurde std::index_sequence_for<Ts...>(). Und in C ++ 17 sind viele andere gute Dinge in die Bibliothek integriert - insbesondere einschließlich std::applyund std::make_from_tuple, die das Entpacken und Aufrufen von Bits behandeln würden
underscore_d
61

Die C ++ 17-Lösung ist einfach zu verwenden std::apply:

auto f = [](int a, double b, std::string c) { std::cout<<a<<" "<<b<<" "<<c<< std::endl; };
auto params = std::make_tuple(1,2.0,"Hello");
std::apply(f, params);

Ich hatte nur das Gefühl, dass dies einmal in einer Antwort in diesem Thread angegeben werden sollte (nachdem es bereits in einem der Kommentare erschienen ist).


Die grundlegende C ++ 14-Lösung fehlt in diesem Thread noch. EDIT: Nein, es ist tatsächlich in der Antwort von Walter.

Diese Funktion ist gegeben:

void f(int a, double b, void* c)
{
      std::cout << a << ":" << b << ":" << c << std::endl;
}

Nennen Sie es mit folgendem Snippet:

template<typename Function, typename Tuple, size_t ... I>
auto call(Function f, Tuple t, std::index_sequence<I ...>)
{
     return f(std::get<I>(t) ...);
}

template<typename Function, typename Tuple>
auto call(Function f, Tuple t)
{
    static constexpr auto size = std::tuple_size<Tuple>::value;
    return call(f, t, std::make_index_sequence<size>{});
}

Beispiel:

int main()
{
    std::tuple<int, double, int*> t;
    //or std::array<int, 3> t;
    //or std::pair<int, double> t;
    call(f, t);    
}

DEMO

Davidhigh
quelle
Ich kann diese Demo nicht mit intelligenten Zeigern zum Laufen bringen - was ist hier falsch? http://coliru.stacked-crooked.com/a/8ea8bcc878efc3cb
Xeverous
@Xeverous: möchtest du so etwas hier bekommen ?
Davidhigh
danke, ich habe 2 Fragen: 1. Warum kann ich nicht std::make_uniquedirekt bestehen? Benötigt es eine konkrete Funktionsinstanz? 2. Warum , std::move(ts)...wenn wir ändern [](auto... ts)zu [](auto&&... ts)?
Xeverous
@Xeverous: 1. funktioniert nicht anhand der Signaturen: Sie std::make_uniqueerwarten ein Tupel, und ein Tupel kann aus einem entpackten Tupel nur über einen anderen Aufruf von erstellt werden std::make_tuple. Dies ist, was ich im Lambda getan habe (obwohl es sehr redundant ist, da Sie das Tupel auch einfach ohne Verwendung in den eindeutigen Zeiger kopieren können call).
Davidhigh
1
Dies sollte nun die Antwort sein.
Fureeish
44

Dies ist eine vollständig kompilierbare Version von Johannes 'Lösung für Awoodlands Frage, in der Hoffnung, dass sie für jemanden nützlich sein kann. Dies wurde mit einem Snapshot von g ++ 4.7 auf Debian Squeeze getestet.

###################
johannes.cc
###################
#include <tuple>
#include <iostream>
using std::cout;
using std::endl;

template<int ...> struct seq {};

template<int N, int ...S> struct gens : gens<N-1, N-1, S...> {};

template<int ...S> struct gens<0, S...>{ typedef seq<S...> type; };

double foo(int x, float y, double z)
{
  return x + y + z;
}

template <typename ...Args>
struct save_it_for_later
{
  std::tuple<Args...> params;
  double (*func)(Args...);

  double delayed_dispatch()
  {
    return callFunc(typename gens<sizeof...(Args)>::type());
  }

  template<int ...S>
  double callFunc(seq<S...>)
  {
    return func(std::get<S>(params) ...);
  }
};

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
#pragma GCC diagnostic ignored "-Wunused-variable"
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
int main(void)
{
  gens<10> g;
  gens<10>::type s;
  std::tuple<int, float, double> t = std::make_tuple(1, 1.2, 5);
  save_it_for_later<int,float, double> saved = {t, foo};
  cout << saved.delayed_dispatch() << endl;
}
#pragma GCC diagnostic pop

Man kann die folgende SConstruct-Datei verwenden

#####################
SConstruct
#####################
#!/usr/bin/python

env = Environment(CXX="g++-4.7", CXXFLAGS="-Wall -Werror -g -O3 -std=c++11")
env.Program(target="johannes", source=["johannes.cc"])

Auf meiner Maschine gibt dies

g++-4.7 -o johannes.o -c -Wall -Werror -g -O3 -std=c++11 johannes.cc
g++-4.7 -o johannes johannes.o
Faheem Mitha
quelle
Warum brauchen Sie die Variablen s und g?
Shoosh
@shoosh Ich denke, sie werden nicht benötigt. Ich vergesse, warum ich diese hinzugefügt habe; Es ist fast drei Jahre her. Aber ich nehme an, um zu zeigen, dass die Instanziierung funktioniert.
Faheem Mitha
42

Hier ist eine C ++ 14-Lösung.

template <typename ...Args>
struct save_it_for_later
{
  std::tuple<Args...> params;
  void (*func)(Args...);

  template<std::size_t ...I>
  void call_func(std::index_sequence<I...>)
  { func(std::get<I>(params)...); }
  void delayed_dispatch()
  { call_func(std::index_sequence_for<Args...>{}); }
};

Dies benötigt noch eine Hilfsfunktion ( call_func). Da dies eine gängige Redewendung ist, sollte der Standard sie std::callmöglicherweise wie bei einer möglichen Implementierung direkt unterstützen

// helper class
template<typename R, template<typename...> class Params, typename... Args, std::size_t... I>
R call_helper(std::function<R(Args...)> const&func, Params<Args...> const&params, std::index_sequence<I...>)
{ return func(std::get<I>(params)...); }

// "return func(params...)"
template<typename R, template<typename...> class Params, typename... Args>
R call(std::function<R(Args...)> const&func, Params<Args...> const&params)
{ return call_helper(func,params,std::index_sequence_for<Args...>{}); }

Dann wird unser verspäteter Versand

template <typename ...Args>
struct save_it_for_later
{
  std::tuple<Args...> params;
  std::function<void(Args...)> func;
  void delayed_dispatch()
  { std::call(func,params); }
};
Walter
quelle
8
Upvoted für die (vorgeschlagene) Umsetzung von std::call. Der chaotische Zoo integer_sequenceund die Hilfstypen von C ++ 14 werden index_sequencehier erklärt: en.cppreference.com/w/cpp/utility/integer_sequence Beachten Sie das auffällige Fehlen von std::make_index_sequence(Args...), weshalb Walter in die klobigere Syntax gezwungen wurde std::index_sequence_for<Args...>{}.
Quuxplusone
3
Und anscheinend seit 3/2016 als std :: apply (func, tup) in C ++ 17 gestimmt: gestimmt en.cppreference.com/w/cpp/utility/apply
ddevienne
18

Dies ist etwas kompliziert zu erreichen (obwohl es möglich ist). Ich rate Ihnen , eine Bibliothek zu verwenden , wo dies bereits umgesetzt wird, nämlich Boost.Fusion (die invoke - Funktion). Als Bonus funktioniert Boost Fusion auch mit C ++ 03-Compilern.

Karel Petranek
quelle
7

Lösung. Zunächst einige Utility Boilerplate:

template<std::size_t...Is>
auto index_over(std::index_sequence<Is...>){
  return [](auto&&f)->decltype(auto){
    return decltype(f)(f)( std::integral_constant<std::size_t, Is>{}... );
  };
}
template<std::size_t N>
auto index_upto(std::integral_constant<std::size_t, N> ={}){
  return index_over( std::make_index_sequence<N>{} );
}

Mit diesen können Sie ein Lambda mit einer Reihe von Ganzzahlen zur Kompilierungszeit aufrufen.

void delayed_dispatch() {
  auto indexer = index_upto<sizeof...(Args)>();
  indexer([&](auto...Is){
    func(std::get<Is>(params)...);
  });
}

und wir sind fertig.

index_uptound index_overSie können mit Parameterpaketen arbeiten, ohne neue externe Überladungen generieren zu müssen.

Natürlich in Sie gerade

void delayed_dispatch() {
  std::apply( func, params );
}

Nun, wenn uns das gefällt, in wir können schreiben:

namespace notstd {
  template<class T>
  constexpr auto tuple_size_v = std::tuple_size<T>::value;
  template<class F, class Tuple>
  decltype(auto) apply( F&& f, Tuple&& tup ) {
    auto indexer = index_upto<
      tuple_size_v<std::remove_reference_t<Tuple>>
    >();
    return indexer(
      [&](auto...Is)->decltype(auto) {
        return std::forward<F>(f)(
          std::get<Is>(std::forward<Tuple>(tup))...
        );
      }
    );
  }
}

relativ leicht und den Reiniger bekommen Syntax versandbereit.

void delayed_dispatch() {
  notstd::apply( func, params );
}

ersetzen Sie einfach notstdmit , stdwenn der Compiler - Upgrades und Bob ist dein Onkel.

Yakk - Adam Nevraumont
quelle
std::apply<- Musik in meinen Ohren
Flexo
@Flexo Nur etwas kürzer als index_uptound weniger flexibel. ;) Versuchen Sie fordern funcmit den Argumenten nach hinten index_uptound std::applysind. Zugegeben, wer zum Teufel will eine Funktion von einem Tupel rückwärts aufrufen.
Yakk - Adam Nevraumont
Kleiner Punkt: std::tuple_size_vist C ++ 17, also für die C ++ 14-Lösung, die durchtypename std::tuple_size<foo>::value
basteln
@ Basteln Ich hoffe, es valueist kein Typ. Aber trotzdem behoben.
Yakk - Adam Nevraumont
@ Yakk Nein, das ist es sizeof...(Types). Ich mag deine Lösung ohne die typename.
Basteln
3

Wenn ich über das Problem nachdenke, basierend auf der gegebenen Antwort, habe ich einen anderen Weg gefunden, das gleiche Problem zu lösen:

template <int N, int M, typename D>
struct call_or_recurse;

template <typename ...Types>
struct dispatcher {
  template <typename F, typename ...Args>
  static void impl(F f, const std::tuple<Types...>& params, Args... args) {
     call_or_recurse<sizeof...(Args), sizeof...(Types), dispatcher<Types...> >::call(f, params, args...);
  }
};

template <int N, int M, typename D>
struct call_or_recurse {
  // recurse again
  template <typename F, typename T, typename ...Args>
  static void call(F f, const T& t, Args... args) {
     D::template impl(f, t, std::get<M-(N+1)>(t), args...);
  }
};

template <int N, typename D>
struct call_or_recurse<N,N,D> {
  // do the call
  template <typename F, typename T, typename ...Args>
  static void call(F f, const T&, Args... args) {
     f(args...);
  }
};

Was erfordert, die Implementierung von delayed_dispatch()zu ändern :

  void delayed_dispatch() {
     dispatcher<Args...>::impl(func, params);
  }

Dies funktioniert, indem das rekursiv std::tuplein ein eigenständiges Parameterpaket konvertiert wird . call_or_recursewird als Spezialisierung benötigt, um die Rekursion mit dem realen Aufruf zu beenden, der nur das fertige Parameterpaket entpackt.

Ich bin mir nicht sicher, ob dies eine "bessere" Lösung ist, aber es ist eine andere Art, darüber nachzudenken und es zu lösen.


Als weitere alternative Lösung können Sie enable_ifetwas verwenden, das wohl einfacher ist als meine vorherige Lösung:

#include <iostream>
#include <functional>
#include <tuple>

void f(int a, double b, void* c) {
  std::cout << a << ":" << b << ":" << c << std::endl;
}

template <typename ...Args>
struct save_it_for_later {
  std::tuple<Args...> params;
  void (*func)(Args...);

  template <typename ...Actual>
  typename std::enable_if<sizeof...(Actual) != sizeof...(Args)>::type
  delayed_dispatch(Actual&& ...a) {
    delayed_dispatch(std::forward<Actual>(a)..., std::get<sizeof...(Actual)>(params));
  }

  void delayed_dispatch(Args ...args) {
    func(args...);
  }
};

int main() {
  int a=666;
  double b = -1.234;
  void *c = NULL;

  save_it_for_later<int,double,void*> saved = {
                                 std::tuple<int,double,void*>(a,b,c), f};
  saved.delayed_dispatch();
}

Die erste Überladung nimmt nur ein weiteres Argument aus dem Tupel und fügt es in ein Parameterpaket ein. Die zweite Überlastung nimmt ein passendes Parameterpaket und führt dann den eigentlichen Aufruf durch, wobei die erste Überlastung in dem einzigen Fall deaktiviert wird, in dem die zweite realisierbar wäre.

Flexo
quelle
1
Ich habe vor einiger Zeit an etwas ähnlich Ähnlichem gearbeitet. Wenn ich Zeit habe, werde ich einen zweiten Blick darauf werfen und sehen, wie es mit den aktuellen Antworten verglichen wird.
Michael Price
@MichaelPrice - rein aus der Lernperspektive wäre ich daran interessiert, alternative Lösungen zu sehen, die nicht auf einen schrecklichen Hack hinauslaufen, der den Stapelzeiger verpfuscht (oder auf ähnliche Weise konventionsspezifische Tricks nennt).
Flexo
2

Meine Variation der Lösung von Johannes unter Verwendung von C ++ 14 std :: index_sequence (und Funktionsrückgabetyp als Vorlagenparameter RetT):

template <typename RetT, typename ...Args>
struct save_it_for_later
{
    RetT (*func)(Args...);
    std::tuple<Args...> params;

    save_it_for_later(RetT (*f)(Args...), std::tuple<Args...> par) : func { f }, params { par } {}

    RetT delayed_dispatch()
    {
        return callFunc(std::index_sequence_for<Args...>{});
    }

    template<std::size_t... Is>
    RetT callFunc(std::index_sequence<Is...>)
    {
        return func(std::get<Is>(params) ...);
    }
};

double foo(int x, float y, double z)
{
  return x + y + z;
}

int testTuple(void)
{
  std::tuple<int, float, double> t = std::make_tuple(1, 1.2, 5);
  save_it_for_later<double, int, float, double> saved (&foo, t);
  cout << saved.delayed_dispatch() << endl;
  return 0;
}
schwart
quelle
All diese Lösungen können das anfängliche Problem lösen, aber ehrlich gesagt, geht dieses Vorlagenmaterial nicht in eine falsche Richtung - in Bezug auf Einfachheit und Wartbarkeit ?
xy
Ich denke, Vorlagen wurden mit C ++ 11 und 14 viel besser und verständlicher. Als ich vor einigen Jahren sah, was Boost mit Vorlagen unter der Haube macht, wurde ich wirklich entmutigt. Ich stimme zu, dass die Entwicklung guter Vorlagen wesentlich schwieriger ist als nur deren Verwendung.
schwart
1
@xy Erstens ist dies in Bezug auf die Komplexität der Vorlagen nichts . Zweitens sind die meisten Vorlagen für Helfer eine Anfangsinvestition für eine Tonne Zeitersparnis, wenn sie später instanziiert werden. Und was möchten Sie lieber nicht tun, was Sie mit Vorlagen tun können? Sie könnten es einfach nicht verwenden und keine irrelevanten Kommentare hinterlassen, die andere Programmierer zu überwachen scheinen.
underscore_d