Bagaimana cara menyimpan argumen template variadic?

89

Apakah mungkin untuk menyimpan paket parameter untuk digunakan nanti?

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

Kemudian nanti:

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

    //...

    add.act();
}
Eric B
sumber
3
Iya; menyimpan tupel dan menggunakan semacam paket indeks untuk melakukan panggilan.
Kerrek SB
Apakah ini menjawab pertanyaan Anda? C ++ Cara menyimpan paket parameter sebagai variabel
user202729

Jawaban:

67

Untuk mencapai apa yang ingin Anda lakukan di sini, Anda harus menyimpan argumen template Anda dalam tupel:

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

Selanjutnya, Anda harus sedikit mengubah konstruktor Anda. Secara khusus, menginisialisasi argsdengan std::make_tupledan juga mengizinkan referensi universal dalam daftar parameter Anda:

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

Selain itu, Anda harus menyiapkan generator urutan seperti ini:

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

Dan Anda dapat mengimplementasikan metode Anda dalam hal menggunakan generator seperti itu:

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

Dan itu dia! Jadi sekarang kelas Anda akan terlihat seperti ini:

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

Ini adalah program lengkap Anda di Coliru.


Update: Berikut adalah metode pembantu yang tidak memerlukan spesifikasi argumen template:

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

Dan lagi, ini demo lainnya.

0x499602D2
sumber
1
dapatkah Anda mengembangkannya sedikit dalam jawaban Anda?
Eric B
1
Karena Ts ... adalah parameter templat kelas, bukan parameter templat fungsi, Ts && ... tidak menentukan paket referensi universal karena tidak ada pengurangan jenis yang terjadi untuk paket parameter. @jogojapan menunjukkan cara yang benar untuk memastikan Anda dapat meneruskan referensi universal ke konstruktor.
masrtis
3
Hati-hati terhadap referensi dan masa pakai objek! void print(const std::string&); std::string hello(); auto act = make_action(print, hello());tidak bagus. Saya lebih suka perilaku dari std::bind, yang membuat salinan dari setiap argumen kecuali Anda menonaktifkannya dengan std::refatau std::cref.
aschepler
1
Menurut saya @jogojapan memiliki solusi yang lebih ringkas dan mudah dibaca.
Tim Kuipers
1
@Riddick Ubah args(std::make_tuple(std::forward<Args>(args)...))ke args(std::forward<Args>(args)...). BTW Saya menulis ini sejak lama dan saya tidak akan menggunakan kode ini untuk tujuan mengikat fungsi ke beberapa argumen. Saya hanya akan menggunakan std::invoke()atau std::apply()saat ini.
0x499602D2
23

Anda bisa menggunakan std::bind(f,args...)untuk ini. Ini akan menghasilkan objek yang dapat dipindahkan dan mungkin dapat disalin yang menyimpan salinan objek fungsi dan masing-masing argumen untuk digunakan nanti:

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

Perhatikan bahwa itu std::bindadalah fungsi dan Anda perlu menyimpan, sebagai anggota data, hasil pemanggilannya. Tipe data dari hasil tersebut tidak mudah untuk diprediksi (Standar bahkan tidak menentukannya secara tepat), jadi saya menggunakan kombinasi dari decltypedan std::declvaluntuk menghitung tipe data tersebut pada waktu kompilasi. Lihat definisi di Action::bind_typeatas.

Juga perhatikan bagaimana saya menggunakan referensi universal di konstruktor template. Ini memastikan bahwa Anda dapat meneruskan argumen yang tidak sama T...persis dengan parameter template kelas (misalnya, Anda dapat menggunakan referensi rvalue ke beberapa Tdan Anda akan meneruskannya apa adanya ke bindpanggilan.)

Catatan terakhir: Jika Anda ingin menyimpan argumen sebagai referensi (sehingga fungsi yang Anda berikan dapat memodifikasi, daripada hanya menggunakannya, argumen tersebut), Anda perlu menggunakannya std::refuntuk menggabungkannya dalam objek referensi. Meneruskan a saja T &akan membuat salinan nilai, bukan referensi.

Kode operasional di Coliru

jogojapan
sumber
Bukankah berbahaya untuk mengikat nilai r? Bukankah itu akan menjadi tidak valid ketika adddidefinisikan dalam lingkup yang berbeda lalu di mana act()dipanggil? Tidak harus konstruktor mendapatkan ConstrT&... argsbukan ConstrT&&... args?
Tim Kuipers
1
@Angelorf Maaf atas balasan saya yang terlambat. Maksud Anda rvalues ​​dalam panggilan ke bind()? Karena bind()dijamin akan membuat salinan (atau pindah ke objek yang baru dibuat), saya rasa tidak akan ada masalah.
jogojapan
@jogojapan Catatan singkat, MSVC17 membutuhkan fungsi dalam konstruktor untuk diteruskan ke bind_ juga (yaitu bind_ (std :: forward <std :: function <void (T ...) >> (f), std :: forward < ConstrT> (args) ...))
Dikalahkan
1
Dalam penginisialisasi, bind_(f, std::forward<ConstrT>(args)...)adalah perilaku tidak terdefinisi sesuai dengan standar, karena konstruktor tersebut ditentukan oleh implementasi. bind_typeditentukan untuk disalin dan / atau dapat dipindahkan, jadi bind_{std::bind(f, std::forward<ConstrT>(args)...)}harus tetap berfungsi.
joshtch
4

Pertanyaan ini berasal dari C ++ 11 hari. Tetapi bagi mereka yang menemukannya di hasil pencarian sekarang, beberapa pembaruan:

Seorang std::tupleanggota masih merupakan cara yang mudah untuk menyimpan argumen secara umum. ( std::bindSolusi yang mirip dengan @ jogojapan juga akan berfungsi jika Anda hanya ingin memanggil fungsi tertentu, tetapi tidak jika Anda ingin mengakses argumen dengan cara lain, atau meneruskan argumen ke lebih dari satu fungsi, dll.)

Di C ++ 14 dan yang lebih baru, std::make_index_sequence<N>ataustd::index_sequence_for<Pack...> dapat menggantikan helper::gen_seq<N>alat yang terlihat di solusi 0x499602D2 :

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

Di C ++ 17 dan yang lebih baru, std::applydapat digunakan untuk menangani pembongkaran tupel:

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

Berikut adalah program C ++ 17 lengkap yang menunjukkan implementasi yang disederhanakan. Saya juga memperbarui make_actionuntuk menghindari jenis referensi di tuple, yang selalu buruk untuk argumen nilai r dan cukup berisiko untuk argumen nilai l.

aschepler
sumber
3

Saya pikir Anda memiliki masalah XY. Mengapa repot-repot menyimpan paket parameter ketika Anda bisa saja menggunakan lambda di situs panggilan? yaitu,

#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();
}
Casey
sumber
Karena dalam aplikasi saya, Tindakan sebenarnya memiliki 3 fungsi berbeda yang semuanya terkait, dan saya lebih suka kelas yang berisi itu berisi 1 Tindakan, dan bukan 3 std :: function
Eric B