"Spacchettare" una tupla per chiamare un puntatore a funzione corrispondente


259

Sto cercando di memorizzare in un std::tuplenumero variabile di valori, che verranno successivamente utilizzati come argomenti per una chiamata a un puntatore a funzione che corrisponde ai tipi memorizzati.

Ho creato un esempio semplificato che mostra il problema che sto lottando per risolvere:

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

Normalmente per problemi che coinvolgono std::tupleo modelli variadici, scriverei un altro modello template <typename Head, typename ...Tail>per valutare ricorsivamente tutti i tipi uno per uno, ma non vedo un modo per farlo per inviare una chiamata di funzione.

La vera motivazione per questo è un po 'più complessa ed è per lo più solo un esercizio di apprendimento comunque. Si può presumere che mi sia stata consegnata la tupla per contratto da un'altra interfaccia, quindi non può essere modificata ma il desiderio di decomprimerlo in una chiamata di funzione è mio. Questo esclude l'utilizzo std::bindcome un modo economico per eludere il problema sottostante.

Qual è un modo pulito per inviare la chiamata utilizzando il std::tuple, o un modo migliore alternativo per ottenere lo stesso risultato netto di memorizzare / inoltrare alcuni valori e un puntatore a funzione fino a un punto futuro arbitrario?


5
Perché non puoi semplicemente usare auto saved = std::bind(f, a, b, c);... e poi chiamare saved()?
Charles Salvia

Non sempre la mia interfaccia da controllare. Ricevo una tupla per contratto da qualcun altro e desidero fare le cose con essa successivamente.
Flexo

Risposte:


68

La soluzione C ++ 17 è semplicemente da usare 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);

Ho appena sentito che dovrebbe essere affermato una volta in una risposta in questo thread (dopo che è già apparso in uno dei commenti).


La soluzione C ++ 14 di base è ancora mancante in questo thread. EDIT: No, in realtà è lì nella risposta di Walter.

Questa funzione è data:

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

Chiamalo con il seguente frammento:

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

Esempio:

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

DEMO


Non riesco a far funzionare questa demo con i puntatori intelligenti: cosa c'è che non va qui? http://coliru.stacked-crooked.com/a/8ea8bcc878efc3cb
Xeverous

@Xeverous: vuoi ottenere qualcosa di simile qui ?
davidhigh

grazie, ho 2 domande: 1. Perché non posso passare std::make_uniquedirettamente? Ha bisogno di un'istanza di funzione concreta? 2. Perché std::move(ts)...se possiamo cambiare [](auto... ts)in [](auto&&... ts)?
Xeverous

@Xeverous: 1. non funziona dalle firme: ci si std::make_uniqueaspetta una tupla e una tupla può essere creata da una tupla decompressa solo tramite un'altra chiamata a std::make_tuple. Questo è ciò che ho fatto in lambda (sebbene sia altamente ridondante, poiché puoi anche semplicemente copiare la tupla nel puntatore univoco senza alcun uso per call).
davidhigh

1
Questa dovrebbe ora essere la risposta.
Fureeish,

277

È necessario creare un pacchetto di numeri di parametri e scompattarli

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

4
Wow, non sapevo che l'operatore di spacchettamento potesse essere usato in questo modo, è carino!
Luc Touraille

5
Johannes, mi rendo conto che sono passati più di 2 anni da quando l'hai postato, ma l'unica cosa con cui sto lottando è la struct gensdefinizione generica (quella che eredita da una derivazione espansa dello stesso). Vedo che alla fine raggiunge la specializzazione con 0. Se l'umore ti si addice e hai i cicli liberi, se puoi ampliare quello e come viene utilizzato per questo, te ne sarei eternamente grato. E vorrei poter votare questo cento volte. Mi sono divertito di più a giocare con le tangenti di questo codice. Grazie.
WhozCraig

22
@ WhozCraig: Quello che fa è generare un tipo seq<0, 1, .., N-1>. Come funziona: 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>. L'ultimo tipo è specializzato, creando seq<0, 1, 2, 3, 4>. Trucco abbastanza intelligente.
mindvirus

2
@NirFriedman: Certo, basta sostituire la versione non specializzata di gens:template <int N, int... S> struct gens { typedef typename gens<N-1, N-1, S...>::type type; };
marton78

11
Vale la pena fare eco alla risposta di Walter e ai commenti al riguardo: la gente non ha più bisogno di inventare le proprie ruote. La generazione di una sequenza era così comune che era standardizzata in C ++ 14 come std::integer_sequence<T, N>e la sua specializzazione per std::size_t, std::index_sequence<N>- più le loro funzioni di supporto associate std::make_in(teger|dex)_sequence<>()e std::index_sequence_for<Ts...>(). E in C ++ 17 ci sono molte altre cose buone integrate nella libreria, in particolare tra cui std::applye std::make_from_tuple, che gestirà lo spacchettamento e la chiamata dei bit
underscore_d

44

Questa è una versione compilabile completa della soluzione di Johannes alla domanda di awoodland, nella speranza che possa essere utile a qualcuno. Questo è stato testato con un'istantanea di g ++ 4.7 su Debian squeeze.

###################
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

Si può usare il seguente file SConstruct

#####################
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"])

Sulla mia macchina, questo dà

g++-4.7 -o johannes.o -c -Wall -Werror -g -O3 -std=c++11 johannes.cc
g++-4.7 -o johannes johannes.o

Perché hai bisogno delle variabili se g?
shoosh

@shoosh immagino che non siano necessari. Non ricordo perché li ho aggiunti; sono passati quasi tre anni. Ma suppongo, per dimostrare che l'istanza funziona.
Faheem Mitha

42

Ecco una soluzione C ++ 14.

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

Ciò richiede ancora una funzione di supporto ( call_func). Poiché questo è un linguaggio comune, forse lo standard dovrebbe supportarlo direttamente come std::callcon possibile implementazione

// 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...>{}); }

Quindi la nostra spedizione ritardata diventa

template <typename ...Args>
struct save_it_for_later
{
  std::tuple<Args...> params;
  std::function<void(Args...)> func;
  void delayed_dispatch()
  { std::call(func,params); }
};

8
Votato per l'implementazione (proposta) di std::call. Lo zoo caotico di C ++ 14 integer_sequencee i index_sequencetipi helper sono spiegati qui: en.cppreference.com/w/cpp/utility/integer_sequence Notare la notevole assenza di std::make_index_sequence(Args...), motivo per cui Walter è stato costretto a utilizzare la sintassi più complicata std::index_sequence_for<Args...>{}.
Quuxplusone

3
E apparentemente votato in C ++ 17 dal 3/2016 come std :: apply (func, tup
ddevienne

18

Questo è un po 'complicato da ottenere (anche se è possibile). Ti consiglio di utilizzare una libreria dove questa è già implementata, ovvero Boost.Fusion (la funzione invoke ). Come bonus, Boost Fusion funziona anche con i compilatori C ++ 03.


7

soluzione. Innanzitutto, alcuni boilerplate di utilità:

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

Questi ti consentono di chiamare un lambda con una serie di interi in fase di compilazione.

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

e abbiamo finito.

index_uptoe index_overconsentono di lavorare con i pacchetti di parametri senza dover generare un nuovo sovraccarico esterno.

Ovviamente in hai appena

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

Ora, se ci piace, in possiamo scrivere:

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

relativamente facilmente e prendi il pulitore sintassi pronta per la spedizione.

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

sostituisci semplicemente notstdcon stdquando il tuo compilatore si aggiorna e bob è tuo zio.


std::apply<- musica per le mie orecchie
Flexo

@Flexo Solo un po 'più corto index_uptoe meno flessibile. ;) Prova a chiamare funccon gli argomenti all'indietro con index_uptoe std::applyrispettivamente. Certo, chi diavolo vuole invocare una funzione da una tupla all'indietro.
Yakk - Adam Nevraumont

Punto minore: std::tuple_size_vè C ++ 17, quindi per la soluzione C ++ 14 che dovrebbe essere sostituita datypename std::tuple_size<foo>::value
basteln

@basteln spero valuenon sia un tipo. Ma risolto comunque.
Yakk - Adam Nevraumont,

@Yakk No, lo è sizeof...(Types). Mi piace la tua soluzione senza typename.
basteln

3

Riflettendo ancora un po 'sul problema in base alla risposta data, ho trovato un altro modo per risolvere lo stesso problema:

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

Ciò richiede la modifica dell'implementazione di delayed_dispatch()in:

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

Funziona convertendo ricorsivamente std::tuplein un pacchetto di parametri a sé stante. call_or_recurseè necessaria come specializzazione per terminare la ricorsione con la chiamata reale, che decomprime semplicemente il pacchetto di parametri completato.

Non sono sicuro che sia comunque una soluzione "migliore", ma è un altro modo di pensarla e risolverla.


Come un'altra soluzione alternativa che puoi usare enable_if, per formare qualcosa di probabilmente più semplice della mia soluzione precedente:

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

Il primo overload prende solo un altro argomento dalla tupla e lo inserisce in un pacchetto di parametri. Il secondo sovraccarico prende un pacchetto di parametri corrispondenti e quindi effettua la chiamata reale, con il primo sovraccarico disabilitato nell'unico caso in cui il secondo sarebbe valido.


1
Ho lavorato a qualcosa di terribilmente simile a questo tempo fa. Se ho tempo, vado a dare una seconda occhiata e vedo come si confronta con le risposte attuali.
Michael Price

@MichaelPrice - puramente dal punto di vista dell'apprendimento, sarei interessato a vedere soluzioni alternative che non si riducano a qualche terribile hack che rovina il puntatore dello stack (o allo stesso modo chiama trucchi specifici della convenzione).
Flexo

2

La mia variazione della soluzione da Johannes utilizzando il C ++ 14 std :: index_sequence (e il tipo di ritorno della funzione come parametro del modello 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;
}

Tutte queste soluzioni possono risolvere il problema iniziale, ma onestamente ragazzi, questa roba del modello non sta andando nella direzione sbagliata, in termini di semplicità e manutenibilità ?
xy

Penso che i modelli siano diventati molto migliori e più comprensibili con C ++ 11 e 14. Alcuni anni fa, quando ho guardato cosa rende il boost con i modelli sotto il cofano, mi sono davvero scoraggiato. Sono d'accordo che lo sviluppo di buoni modelli è significativamente più difficile del semplice utilizzo.
schwart

1
@xy In primo luogo, in termini di complessità del modello, questo non è niente . In secondo luogo, la maggior parte dei modelli di helper rappresenta un investimento iniziale per una tonnellata di tempo risparmiato durante la creazione di un'istanza successiva. Infine, cosa, preferiresti non avere la possibilità di fare ciò che i modelli ti consentono di fare? Potresti semplicemente non usarlo e non lasciare commenti irrilevanti che sembrano controllare altri programmatori.
underscore_d
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.