“Membongkar” sebuah tuple untuk memanggil pointer fungsi yang cocok


255

Saya mencoba untuk menyimpan dalam std::tupleberbagai jumlah nilai, yang nantinya akan digunakan sebagai argumen untuk panggilan ke pointer fungsi yang cocok dengan tipe yang disimpan.

Saya telah membuat contoh sederhana yang menunjukkan masalah yang saya sedang berjuang untuk pecahkan:

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

Biasanya untuk masalah yang melibatkan std::tupleatau templat variadik, saya akan menulis templat lain yang ingin template <typename Head, typename ...Tail>secara rekursif mengevaluasi semua jenis satu per satu, tetapi saya tidak dapat melihat cara melakukan hal itu untuk mengirim panggilan fungsi.

Motivasi nyata untuk ini agak lebih kompleks dan itu sebagian besar hanya latihan belajar saja. Anda dapat mengasumsikan bahwa saya menyerahkan tuple dengan kontrak dari antarmuka lain, jadi tidak dapat diubah tetapi keinginan untuk membukanya menjadi panggilan fungsi adalah milik saya. Ini std::bindmengesampingkan penggunaan sebagai cara murah untuk menghindari masalah mendasar.

Apa cara bersih pengiriman panggilan menggunakan std::tuple, atau cara alternatif yang lebih baik untuk mencapai hasil bersih yang sama dari menyimpan / meneruskan beberapa nilai dan penunjuk fungsi sampai titik masa depan yang sewenang-wenang?


5
Mengapa Anda tidak bisa menggunakan auto saved = std::bind(f, a, b, c);... lalu menelepon saved()?
Charles Salvia

Tidak selalu antarmuka saya untuk dikontrol. Saya menerima tuple berdasarkan kontrak dari orang lain dan ingin melakukan hal-hal dengan itu selanjutnya.
Flexo

Jawaban:


275

Anda perlu membuat paket parameter angka dan membukanya

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, saya tidak tahu operator membongkar dapat digunakan seperti itu, ini bagus!
Luc Touraille

5
Johannes, saya menyadari sudah 2+ tahun sejak Anda memposting ini, tetapi satu hal yang saya perjuangkan adalah struct gensdefinisi generik (definisi yang diturunkan dari derivasi yang diperluas dari kata yang sama). Saya melihatnya pada akhirnya mencapai spesialisasi dengan 0. Jika suasana hati cocok untuk Anda dan Anda memiliki siklus cadangan, jika Anda dapat mengembangkannya, dan bagaimana ia digunakan untuk ini, saya akan berterima kasih selamanya. Dan saya berharap saya bisa memilih ini seratus kali. Saya lebih senang bermain dengan garis singgung dari kode ini. Terima kasih.
WhozCraig

22
@WhozCraig: Apa yang dilakukannya adalah menghasilkan tipe seq<0, 1, .., N-1>. Cara kerjanya: 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>. Jenis terakhir adalah khusus, menciptakan seq<0, 1, 2, 3, 4>. Trik yang cukup pintar.
mindvirus

2
@NirFriedman: Tentu, ganti saja versi yang tidak ditentukan gensoleh:template <int N, int... S> struct gens { typedef typename gens<N-1, N-1, S...>::type type; };
marton78

11
Layak untuk menggemakan jawaban dan komentar Walter di atasnya: rakyat tidak perlu lagi menciptakan roda mereka sendiri. Menghasilkan urutan sangat umum sehingga standar di C ++ 14 sebagai std::integer_sequence<T, N>dan spesialisasi untuk std::size_t, std::index_sequence<N>- ditambah fungsi pembantu terkait std::make_in(teger|dex)_sequence<>()dan std::index_sequence_for<Ts...>(). Dan di C ++ 17 ada banyak hal baik lainnya yang diintegrasikan ke dalam perpustakaan - khususnya termasuk std::applydan std::make_from_tuple, yang akan menangani bit pembongkaran dan pemanggilan
underscore_d

62

Solusi C ++ 17 hanya dengan menggunakan 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);

Hanya merasa itu harus dinyatakan sekali dalam jawaban di utas ini (setelah sudah muncul di salah satu komentar).


Solusi dasar C ++ 14 masih hilang di utas ini. EDIT: Tidak, itu sebenarnya ada di jawaban Walter.

Fungsi ini diberikan:

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

Sebutkan dengan cuplikan berikut:

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

Contoh:

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

DEMO


Saya tidak dapat menjalankan demo ini dengan pointer pintar - ada apa di sini? http://coliru.stacked-crooked.com/a/8ea8bcc878efc3cb
Xeverous

@Xeverous: Anda ingin mendapatkan yang seperti ini di sini ?
davidhigh

terima kasih, saya punya 2 pertanyaan: 1. Mengapa saya tidak bisa lulus std::make_uniquelangsung? Apakah perlu fungsi konkret contoh? 2. Mengapa std::move(ts)...jika kita bisa mengubah [](auto... ts)ke [](auto&&... ts)?
Xeverous

@Xeverous: 1. tidak bekerja dari tanda tangan: Anda std::make_uniquemengharapkan tuple, dan tuple dapat dibuat dari tuple yang tidak dibongkar hanya melalui panggilan lain ke std::make_tuple. Ini adalah apa yang telah saya lakukan di lambda (meskipun sangat redundan, karena Anda juga dapat dengan mudah menyalin tuple ke pointer unik tanpa menggunakan call).
davidhigh

1
Ini sekarang harus yang jawabannya.
Fureeish

44

Ini adalah versi kompilasi yang lengkap dari solusi Johannes untuk pertanyaan awoodland, dengan harapan semoga bermanfaat bagi seseorang. Ini diuji dengan snapshot g ++ 4.7 pada pemerasan Debian.

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

Satu dapat menggunakan file SConstruct berikut

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

Di mesin saya, ini memberi

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

Mengapa Anda membutuhkan variabel s dan g?
shoosh

@shoosh Saya kira mereka tidak diperlukan. Saya lupa mengapa saya menambahkan itu; sudah hampir tiga tahun. Tapi saya kira, untuk menunjukkan bahwa instantiasi bekerja.
Faheem Mitha

42

Berikut ini adalah solusi 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...>{}); }
};

Ini masih membutuhkan satu fungsi pembantu ( call_func). Karena ini adalah idiom umum, mungkin standar harus mendukungnya secara langsung std::calldengan kemungkinan implementasi

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

Kemudian pengiriman tertunda kami menjadi

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
Terpilih untuk implementasi (diusulkan) dari std::call. Kebun binatang C ++ 14 yang kacau integer_sequencedan index_sequencejenis-jenis pembantu dijelaskan di sini: en.cppreference.com/w/cpp/utility/integer_afterence Perhatikan ketiadaan yang mencolok std::make_index_sequence(Args...), itulah sebabnya Walter dipaksa ke dalam sintaksis clunkier std::index_sequence_for<Args...>{}.
Quuxplusone

3
Dan tampaknya memilih ke C ++ 17 sejak 3/2016 sebagai std :: apply (func, tup
ddevienne

18

Ini agak rumit untuk dicapai (meskipun mungkin). Saya menyarankan Anda untuk menggunakan perpustakaan di mana ini sudah diterapkan, yaitu Boost.Fusion ( fungsi panggil ). Sebagai bonus, Boost Fusion juga bekerja dengan kompiler C ++ 03.


7

larutan. Pertama, beberapa utilitas 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>{} );
}

Ini memungkinkan Anda memanggil lambda dengan serangkaian bilangan bulat waktu kompilasi.

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

dan kita selesai.

index_uptodan index_overmembiarkan Anda bekerja dengan paket parameter tanpa harus menghasilkan kelebihan eksternal baru.

Tentu saja di Anda hanya

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

Sekarang, jika kita suka itu, masuk kita dapat menulis:

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

relatif mudah dan bersihkan sintaks siap dikirim.

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

ganti saja notstddengan stdketika kompiler Anda ditingkatkan dan bob adalah paman Anda.


std::apply<- musik di telingaku
Flexo

@Flexo Hanya sedikit lebih pendek index_uptodan kurang fleksibel. ;) Coba panggil funcdengan argumen mundur dengan index_uptodan std::applymasing - masing. Diakui, siapa sih yang ingin memanggil fungsi dari tuple mundur.
Yakk - Adam Nevraumont

Poin minor: std::tuple_size_vadalah C ++ 17, jadi untuk solusi C ++ 14 yang harus diganti olehtypename std::tuple_size<foo>::value
basteln

@Basteln Saya harap valuebukan tipe. Tapi tetap diperbaiki.
Yakk - Adam Nevraumont

@ Ya Tidak, ini sizeof...(Types). Saya suka solusi Anda tanpa typename.
basteln

3

Memikirkan masalah lebih didasarkan pada jawaban yang diberikan, saya telah menemukan cara lain untuk memecahkan masalah yang sama:

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

Yang membutuhkan perubahan implementasi delayed_dispatch()menjadi:

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

Ini berfungsi dengan secara rekursif mengubah std::tuplepaket parameter menjadi haknya sendiri. call_or_recursediperlukan sebagai spesialisasi untuk mengakhiri rekursi dengan panggilan nyata, yang hanya membongkar paket parameter yang sudah selesai.

Saya tidak yakin ini merupakan solusi yang "lebih baik", tetapi ini cara lain untuk memikirkan dan menyelesaikannya.


Sebagai solusi alternatif lain yang dapat Anda gunakan enable_if, untuk membentuk sesuatu yang bisa dibilang lebih sederhana daripada solusi saya sebelumnya:

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

Kelebihan pertama hanya membutuhkan satu argumen lagi dari tuple dan memasukkannya ke dalam paket parameter. Kelebihan kedua mengambil paket parameter yang cocok dan kemudian membuat panggilan nyata, dengan kelebihan pertama dinonaktifkan di satu-satunya kasus di mana yang kedua akan layak.


1
Saya mengerjakan sesuatu yang sangat mirip dengan ini beberapa waktu lalu. Jika saya punya waktu saya akan melihat-lihat lagi dan melihat bagaimana membandingkannya dengan jawaban saat ini.
Michael Harga

@MichaelPrice - murni dari sudut pandang pembelajaran saya akan tertarik melihat solusi alternatif yang tidak mengarah ke beberapa hack yang merusak bot stack (atau dengan cara yang sama memanggil trik khusus konvensi).
Flexo

2

Variasi saya dari solusi dari Johannes menggunakan C ++ 14 std :: index_afterence (dan tipe pengembalian fungsi sebagai parameter templat 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;
}

Semua solusi itu dapat menyelesaikan masalah awal, tetapi jujur ​​saja, bukankah hal-hal template ini mengarah ke arah yang salah - dalam hal kesederhanaan dan pemeliharaan ?
xy

Saya pikir templat menjadi jauh lebih baik dan lebih mudah dipahami dengan C ++ 11 dan 14. Beberapa tahun yang lalu ketika saya melihat apa yang membuat boost dengan templat di bawah tenda, saya menjadi sangat berkecil hati. Saya setuju bahwa mengembangkan templat yang baik secara signifikan lebih sulit daripada hanya menggunakannya.
schwart

1
@xy Pertama, dalam hal kompleksitas templat, ini bukan apa - apa . Kedua, sebagian besar templat pembantu adalah investasi awal untuk satu ton waktu yang dihemat saat instantiasi nanti. Terakhir, apa, apakah Anda lebih suka tidak memiliki kemampuan untuk melakukan template apa yang dapat Anda lakukan? Anda tidak bisa menggunakannya, dan tidak meninggalkan komentar yang tidak relevan yang tampaknya menjadi pemrogram pemrogram lain.
underscore_d
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.