„Rozpakowywanie” krotki w celu wywołania odpowiedniego wskaźnika funkcji


253

Próbuję zapisać w std::tuplezmiennej liczbie wartości, które później zostaną użyte jako argumenty wywołania wskaźnika funkcji, który pasuje do przechowywanych typów.

Stworzyłem uproszczony przykład pokazujący problem, który staram się rozwiązać:

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

Zwykle w przypadku problemów związanych z std::tupleszablonami lub wariantami pisałbym inny szablon, taki jak template <typename Head, typename ...Tail>rekurencyjna ocena wszystkich typów jeden po drugim, ale nie widzę sposobu na wywołanie wywołania funkcji.

Prawdziwa motywacja do tego jest nieco bardziej złożona i w większości to tylko ćwiczenie edukacyjne. Możesz założyć, że krotkę dostałem na podstawie umowy z innego interfejsu, więc nie można jej zmienić, ale chęć rozpakowania jej w wywołanie funkcji jest moja. Wyklucza to użycie std::bindjako taniego sposobu na ominięcie podstawowego problemu.

Jaki jest czysty sposób wysłania wywołania przy użyciu std::tuplelub alternatywny lepszy sposób osiągnięcia tego samego wyniku netto przechowywania / przekazywania niektórych wartości i wskaźnika funkcji do dowolnego punktu w przyszłości?


5
Dlaczego nie możesz po prostu użyć auto saved = std::bind(f, a, b, c);... a potem po prostu zadzwonić saved()?
Charles Salvia,

Nie zawsze mój interfejs do kontrolowania. Otrzymuję krotkę na podstawie umowy od kogoś innego i chcę później z tym zrobić.
Flexo

Odpowiedzi:


275

Musisz zbudować pakiet parametrów liczb i rozpakować je

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, nie wiedziałem, że można użyć takiego operatora rozpakowywania, to miłe!
Luc Touraille

5
Johannes, zdaję sobie sprawę, że minęły ponad dwa lata, odkąd to opublikowałeś, ale jedyną rzeczą, z którą się zmagam, jest struct gensogólna definicja (ta, która dziedziczy po rozszerzonej pochodnej tego samego). Widzę, że ostatecznie osiąga specjalizację z 0. Jeśli nastrój ci odpowiada i masz zapasowe cykle, jeśli możesz rozwinąć tę kwestię i jak ją wykorzystasz, byłbym na zawsze wdzięczny. I żałuję, że nie mogłem głosować więcej niż sto razy. Miałem więcej zabawy grając ze styczną z tego kodu. Dzięki.
WhozCraig,

22
@WhozCraig: Generuje typ seq<0, 1, .., N-1>. Jak to działa: 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>. Ostatni typ specjalizuje się w tworzeniu seq<0, 1, 2, 3, 4>. Całkiem sprytna sztuczka.
mindvirus

2
@NirFriedman: Jasne, po prostu zastąp niespecjalistyczną wersję gens:template <int N, int... S> struct gens { typedef typename gens<N-1, N-1, S...>::type type; };
marton78

11
Warto powtórzyć odpowiedź Waltera i komentarze na jej temat: ludzie nie muszą już wymyślać własnych kół. Generowanie sekwencji było tak powszechne, że została znormalizowana w C ++ 14, jak std::integer_sequence<T, N>i dla ich specjalizacji std::size_t, std::index_sequence<N>- plus związanych z nimi funkcji pomocniczych std::make_in(teger|dex)_sequence<>()i std::index_sequence_for<Ts...>(). A w C ++ 17 jest wiele innych dobrych rzeczy zintegrowanych z biblioteką - w tym w szczególności std::applyi std::make_from_tuple, które obsługiwałyby rozpakowywanie i wywoływanie bitów
underscore_d

61

Rozwiązaniem C ++ 17 jest po prostu użycie 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);

Po prostu czułem, że należy to powiedzieć raz w odpowiedzi w tym wątku (po tym, jak już pojawił się w jednym z komentarzy).


W tym wątku wciąż brakuje podstawowego rozwiązania C ++ 14. EDYCJA: Nie, tak naprawdę jest w odpowiedzi Waltera.

Ta funkcja jest podana:

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

Nazwij go następującym fragmentem:

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

Przykład:

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

PRÓBNY


Nie mogę sprawić, by to demo działało z inteligentnymi wskaźnikami - co jest nie tak? http://coliru.stacked-crooked.com/a/8ea8bcc878efc3cb
Xeverous,

@ Xeverous: czy chcesz dostać coś takiego tutaj ?
davidhigh

dzięki, mam 2 pytania: 1. Dlaczego nie mogę przekazać std::make_uniquebezpośrednio? Czy potrzebuje konkretnej instancji funkcji? 2. Dlaczego std::move(ts)..., jeśli możemy zmienić [](auto... ts)się [](auto&&... ts)?
Xeverous,

@ Xeverous: 1. nie działa na podstawie sygnatur: std::make_uniqueoczekujesz krotki, a krotkę można utworzyć z rozpakowanej krotki tylko za pomocą innego wywołania do std::make_tuple. To właśnie zrobiłem w lambda (chociaż jest bardzo redundantne, ponieważ można również po prostu skopiować krotkę do unikalnego wskaźnika bez użycia call).
davidhigh

1
To powinno być teraz odpowiedź.
Fureeish

44

Jest to w pełni kompilowalna wersja rozwiązania Johannesa do pytania o awoodland, w nadziei, że może być dla kogoś przydatna. Zostało to przetestowane za pomocą migawki g ++ 4.7 na ściśnięciu Debiana.

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

Można użyć następującego pliku 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"])

Na mojej maszynie to daje

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

Dlaczego potrzebujesz zmiennych s i g?
shoosh

@shoosh Chyba nie są potrzebne. Zapominam, dlaczego je dodałem; minęły prawie trzy lata. Ale przypuszczam, że aby pokazać, że tworzenie instancji działa.
Faheem Mitha,

42

Oto rozwiązanie 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...>{}); }
};

To wciąż potrzebuje jednej funkcji pomocniczej ( call_func). Ponieważ jest to powszechny idiom, być może standard powinien go wspierać bezpośrednio, jak w std::callprzypadku możliwej implementacji

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

Wtedy nasza opóźniona wysyłka staje się

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
Głosowano za (proponowane) wdrożenie std::call. Chaotyczne zoo integer_sequencei index_sequencetypy pomocników C ++ 14 zostały wyjaśnione tutaj: en.cppreference.com/w/cpp/utility/integer_sequence Zwróć uwagę na wyraźną nieobecność std::make_index_sequence(Args...), dlatego Walter został zmuszony do stosowania składni clunkier std::index_sequence_for<Args...>{}.
Quuxplusone

3
I najwyraźniej głosował na C ++ 17 od 3/2016 jako std :: Apply (func, tup): en.cppreference.com/w/cpp/utility/apply
ddevienne

18

Jest to nieco skomplikowane do osiągnięcia (nawet jeśli jest to możliwe). Radzę skorzystać z biblioteki, w której jest to już zaimplementowane, a mianowicie Boost.Fusion ( funkcja invoke ). Jako bonus, Boost Fusion współpracuje również z kompilatorami C ++ 03.


6

rozwiązanie. Po pierwsze, płyta użytkowa:

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

Umożliwiają one wywołanie lambda z szeregiem liczb całkowitych w czasie kompilacji.

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

i skończymy.

index_uptoi index_overpozwala pracować z pakietami parametrów bez konieczności generowania nowych zewnętrznych przeciążeń.

Oczywiście w ty tylko

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

Teraz, jeśli nam się to podoba, w możemy pisać:

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

stosunkowo łatwo i zdobądź czystsze Składnia gotowa do wysyłki.

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

wystarczy wymienić notstdze stdkiedy twoi modernizacje kompilatora i Bob jest twoim wujem.


std::apply<- muzyka dla moich uszu
Flexo

@Flexo Tylko nieco krótszy index_uptoi mniej elastyczny. ;) Spróbuj zadzwonić funcz argumentami do tyłu odpowiednio przy pomocy index_uptoi std::apply. Trzeba przyznać, kto, do licha, chce wywołać funkcję od krotki do tyłu.
Yakk - Adam Nevraumont

Drobny punkt: std::tuple_size_vjest C ++ 17, więc dla rozwiązania C ++ 14, które musiałoby zostać zastąpionetypename std::tuple_size<foo>::value
basteln

@ Basteln Mam nadzieję, że valuenie jest typem. Ale i tak naprawione.
Yakk - Adam Nevraumont

@ Yakk Nie, to jest sizeof...(Types). Podoba mi się twoje rozwiązanie bez typename.
basteln

3

Zastanawiając się nad tym problemem w oparciu o udzieloną odpowiedź, znalazłem inny sposób rozwiązania tego samego problemu:

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

Co wymaga zmiany implementacji delayed_dispatch()na:

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

Działa to poprzez rekursywną konwersję std::tupleparametru na sam pakiet parametrów. call_or_recursejest potrzebna jako specjalizacja do zakończenia rekurencji z prawdziwym wywołaniem, które po prostu rozpakowuje skompletowany pakiet parametrów.

Nie jestem pewien, czy to jest i tak „lepsze” rozwiązanie, ale to inny sposób myślenia i rozwiązywania go.


Jako inne alternatywne rozwiązanie, którego możesz użyć enable_if, aby stworzyć coś prawdopodobnie prostszego niż moje poprzednie rozwiązanie:

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

Pierwsze przeciążenie pobiera jeszcze jeden argument z krotki i umieszcza go w pakiecie parametrów. Drugie przeciążenie pobiera pasujący zestaw parametrów, a następnie wykonuje prawdziwe wywołanie, przy czym pierwsze przeciążenie jest wyłączone w jedynym i jedynym przypadku, w którym drugi byłby wykonalny.


1
Jakiś czas temu pracowałem nad czymś okropnie podobnym do tego. Jeśli będę miał czas, przejdę po raz drugi i zobaczę, jak się ma do obecnych odpowiedzi.
Michael Price

@MichaelPrice - czysto z perspektywy uczenia się, chciałbym zobaczyć alternatywne rozwiązania, które nie sprowadzają się do okropnego włamania, który drąży wskaźnik stosu (lub podobnie nazywając sztuczki specyficzne dla konwencji).
Flexo

2

Moja odmiana rozwiązania od Johannesa przy użyciu C ++ 14 std :: index_sequence (i typ zwracanej funkcji jako parametr szablonu 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;
}

Wszystkie te rozwiązania mogą rozwiązać początkowy problem, ale szczerze mówiąc, czy te szablony nie idą w złym kierunku - pod względem prostoty i łatwości konserwacji ?
xy

Myślę, że szablony stały się znacznie lepsze i bardziej zrozumiałe w C ++ 11 i 14. Kilka lat temu, kiedy spojrzałem na to, co robi ulepszenie dzięki szablonom pod maską, bardzo mnie to zniechęciło. Zgadzam się, że opracowanie dobrych szablonów jest znacznie trudniejsze niż samo ich użycie.
schwart

1
@xy Po pierwsze, jeśli chodzi o złożoność szablonów, to nic . Po drugie, większość szablonów pomocników to inwestycja początkowa, która pozwala zaoszczędzić mnóstwo czasu podczas ich późniejszego tworzenia. Na koniec, co wolałbyś nie robić, jakie szablony na to pozwalają? Po prostu nie możesz go użyć i nie pozostawiać nieistotnych komentarzy, które wydają się pilnować innych programistów.
underscore_d
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.