Jak przechowywać różne argumenty szablonów?

89

Czy można w jakiś sposób zapisać pakiet parametrów do późniejszego wykorzystania?

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

Później:

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

    //...

    add.act();
}
Eric B.
źródło
3
Tak; zapisz krotkę i użyj jakiegoś rodzaju pakietu indeksowego, aby wykonać wywołanie.
Kerrek SB
Czy to odpowiada na twoje pytanie? C ++ Jak zapisać pakiet parametrów jako zmienną
user202729

Odpowiedzi:

67

Aby osiągnąć to, co chcesz tutaj zrobić, musisz przechowywać argumenty szablonu w krotce:

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

Ponadto będziesz musiał nieco zmienić swojego konstruktora. W szczególności, inicjalizacja argsza pomocą, std::make_tuplea także zezwolenie na uniwersalne odniesienia na liście parametrów:

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

Co więcej, musiałbyś skonfigurować generator sekwencji podobny do tego:

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

I możesz zaimplementować swoją metodę w ten sposób, że bierze taki generator:

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

I to wszystko! Więc teraz twoja klasa powinna wyglądać tak:

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

Oto Twój pełny program na Coliru.


Aktualizacja: Oto metoda pomocnicza, dzięki której specyfikacja argumentów szablonu nie jest konieczna:

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

I znowu, oto kolejne demo.

0x499602D2
źródło
1
czy mógłbyś to trochę rozwinąć w swojej odpowiedzi?
Eric B
1
Ponieważ Ts ... jest parametrem szablonu klasy, a nie parametrem szablonu funkcji, Ts && ... nie definiuje pakietu odwołań uniwersalnych, ponieważ dla pakietu parametrów nie występuje odliczenie typu. @jogojapan pokazuje poprawny sposób, aby upewnić się, że możesz przekazać uniwersalne odwołania do konstruktora.
masrtis
3
Uważaj na odniesienia i żywotność obiektów! void print(const std::string&); std::string hello(); auto act = make_action(print, hello());nie jest dobrze. Wolałbym zachowanie polecenia std::bind, które tworzy kopię każdego argumentu, chyba że wyłączysz go za pomocą std::reflub std::cref.
aschepler
1
Myślę, że @jogojapan ma o wiele bardziej zwięzłe i czytelne rozwiązanie.
Tim Kuipers
1
@Riddick Zmień args(std::make_tuple(std::forward<Args>(args)...))na args(std::forward<Args>(args)...). Swoją drogą napisałem to dawno temu i nie użyłbym tego kodu do wiązania funkcji z jakimiś argumentami. Po prostu użyłbym std::invoke()lub std::apply()obecnie.
0x499602D2
23

Możesz użyć std::bind(f,args...)do tego. Wygeneruje ruchomy i prawdopodobnie kopiowalny obiekt, który przechowuje kopię obiektu funkcji i każdego z argumentów do późniejszego wykorzystania:

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

Zauważ, że std::bindjest to funkcja i jako element członkowski danych musisz zapisać wynik jej wywołania. Typ danych tego wyniku nie jest łatwy do przewidzenia (norma nawet nie określa go dokładnie), więc używam kombinacji decltypei std::declvaldo obliczania tego typu danych w czasie kompilacji. Zobacz definicję Action::bind_typepowyżej.

Zwróć także uwagę, jak użyłem uniwersalnych odwołań w konstruktorze opartym na szablonach. Gwarantuje to, że możesz przekazywać argumenty, które nie pasują T...dokładnie do parametrów szablonu klasy (np. Możesz użyć odniesień rvalue do niektórych z argumentów, Ta otrzymasz je przekazane w niezmienionej postaci do bindwywołania).

Uwaga końcowa: jeśli chcesz przechowywać argumenty jako odwołania (aby funkcja, którą przekazujesz, mogła je modyfikować, a nie tylko ich używać), musisz użyć ich std::refdo zawijania ich w obiekty referencyjne. Samo przekazanie a T &spowoduje utworzenie kopii wartości, a nie odniesienia.

Kod operacyjny na Coliru

jogojapan
źródło
Czy wiązanie wartości r nie jest niebezpieczne? Czy nie zostałyby one unieważnione, gdyby addzostały zdefiniowane w innym zakresie niż miejsce act()wywołania? Czy konstruktor nie powinien dostać ConstrT&... argszamiast ConstrT&&... args?
Tim Kuipers
1
@Angelorf Przepraszam za spóźnioną odpowiedź. Masz na myśli wartości r w wezwaniu bind()? Ponieważ bind()gwarantujemy wykonanie kopii (lub przeniesienie do świeżo utworzonych obiektów), nie sądzę, że może to być problem.
jogojapan
@jogojapan Szybka uwaga, MSVC17 wymaga, aby funkcja w konstruktorze była również przekazywana do bind_ (tj. bind_ (std :: forward <std :: function <void (T ...) >> (f), std :: forward < ConstrT> (args) ...))
Outshined
1
W inicjatorze bind_(f, std::forward<ConstrT>(args)...)jest niezdefiniowane zachowanie zgodnie ze standardem, ponieważ ten konstruktor jest zdefiniowany w ramach implementacji. bind_typejest określony jako konstruowalny do kopiowania i / lub przenoszenia, więc bind_{std::bind(f, std::forward<ConstrT>(args)...)}nadal powinien działać.
joshtch
4

To pytanie pochodziło z C ++ 11 dni. Ale dla tych, którzy teraz znajdują go w wynikach wyszukiwania, kilka aktualizacji:

Członek std::tuplejest nadal prostym sposobem na ogólne przechowywanie argumentów. ( std::bindRozwiązanie podobne do @ jogojapan zadziała również, jeśli chcesz po prostu wywołać określoną funkcję, ale nie, jeśli chcesz uzyskać dostęp do argumentów w inny sposób lub przekazać argumenty do więcej niż jednej funkcji itp.)

W C ++ 14 i nowszych wersjach std::make_index_sequence<N>lubstd::index_sequence_for<Pack...> może zastąpić helper::gen_seq<N>narzędzie widoczne w rozwiązaniu 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...>{});
    }
    // ...
};

W C ++ 17 i nowszych std::applymożna zająć się rozpakowywaniem krotki:

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

Oto pełny program C ++ 17 pokazujący uproszczoną implementację. Zaktualizowałem również, make_actionaby uniknąć typów referencyjnych w tuple, co zawsze było złe dla argumentów r-wartości i dość ryzykowne dla argumentów l-wartości.

aschepler
źródło
3

Myślę, że masz problem z XY. Po co zadawać sobie tyle trudu, aby przechowywać pakiet parametrów, skoro można po prostu użyć lambdy na stronie wywołania? to znaczy,

#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
źródło
Ponieważ w mojej aplikacji Action faktycznie ma 3 różne funktory, które są ze sobą powiązane, i wolałbym, aby klasy, które ją zawierają, zawierały 1 Action, a nie 3 std :: function
Eric B