Як зберігати аргументи варіатичного шаблону?


89

Чи можна якось зберегти пакет параметрів для подальшого використання?

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

Потім пізніше:

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

    //...

    add.act();
}

3
Так; зберігайте кортеж і використовуйте якийсь індексний пакет для здійснення дзвінка.
Kerrek SB

Це відповідає на ваше запитання? C ++ Як зберігати пакет параметрів як змінну
user202729

Відповіді:


67

Щоб виконати те, що ви хочете зробити тут, вам доведеться зберігати аргументи шаблону в кортежі:

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

Крім того, вам доведеться трохи змінити конструктор. Зокрема, ініціалізація argsза допомогою, std::make_tupleа також дозволяючи універсальні посилання у вашому списку параметрів:

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

Більше того, вам доведеться встановити генератор послідовностей приблизно так:

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

І ви можете реалізувати свій метод з точки зору прийняття такого генератора:

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

І це все! Тож тепер ваш клас повинен виглядати так:

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

Ось ваша повна програма про Coliru.


Оновлення: Ось допоміжний метод, за допомогою якого не потрібно вказувати аргументи шаблону:

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

І знову, ось ще одна демонстрація.


1
Ви могли б трохи розширити це у своїй відповіді?
Eric B

1
Оскільки Ts ... є параметром шаблону класу, а не параметром шаблону функції, Ts && ... не визначає пакет універсальних посилань, оскільки для пакета параметрів не відбувається вирахування типу. @jogojapan показує правильний спосіб забезпечити передачу універсальних посилань конструктору.
masrtis

3
Слідкуйте за посиланнями та життям об’єктів! void print(const std::string&); std::string hello(); auto act = make_action(print, hello());це нічого доброго. Я віддаю перевагу поведінці std::bind, яка робить копію кожного аргументу, якщо ви не вимкнете це за допомогою std::refабо std::cref.
aschepler

1
Я думаю, що у @jogojapan є більш стисле та читабельне рішення.
Тім Кайперс

1
@Riddick Змінити args(std::make_tuple(std::forward<Args>(args)...))на args(std::forward<Args>(args)...). До речі, я писав це давно і не став би використовувати цей код для прив'язки функції до деяких аргументів. Я б просто використовував std::invoke()або std::apply()сьогодні.
0x499602D2

23

Ви можете використовувати std::bind(f,args...)для цього. Він генерує рухомий і, можливо, копіюваний об’єкт, який зберігає копію об’єкта функції та кожного з аргументів для подальшого використання:

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

Зверніть увагу, що std::bindце функція, і вам потрібно зберегти як член даних результат виклику її. Тип даних цього результату передбачити непросто (Стандарт навіть точно не вказує його), тому я використовую комбінацію decltypeта std::declvalдля обчислення цього типу даних під час компіляції. Див. Визначення Action::bind_typeвище.

Також зауважте, як я використовував універсальні посилання в шаблонізованому конструкторі. Це гарантує, що ви можете передавати аргументи, які не T...точно відповідають параметрам шаблону класу (наприклад, ви можете використовувати посилання rvalue на деякі з них, Tі ви отримаєте їх переадресовані як є на bindвиклик).

Заключне зауваження: Якщо ви хочете зберігати аргументи як посилання (щоб передана вами функція могла їх змінювати, а не просто використовувати), вам потрібно використовувати, std::refщоб обернути їх у об'єкти посилання. Просто передача а T &, створить копію значення, а не посилання.

Операційний код на Coliru


Чи не небезпечно пов'язувати значення? Хіба це не стане недійсним, коли addце визначено в іншій області, ніж тоді, де act()це викликано? Чи не повинен конструктор отримати ConstrT&... argsшвидше ніж ConstrT&&... args?
Тім Кайперс,

1
@Angelorf Вибачте за мою пізню відповідь. Ви маєте на увазі значення rvalues ​​у дзвінку bind()? Оскільки bind()гарантується копіювання (або переміщення у щойно створені об’єкти), я не думаю, що проблема може бути.
jogojapan

@jogojapan Коротке зауваження, MSVC17 вимагає, щоб функція в конструкторі також пересилалася на bind_ (тобто bind_ (std :: forward <std :: function <void (T ...) >> (f), std :: forward < ConstrT> (args) ...))
Засвічений

1
В ініціалізаторі bind_(f, std::forward<ConstrT>(args)...)невизначена поведінка відповідно до стандарту, оскільки цей конструктор визначений реалізацією. bind_typeвказано як копіювальний та / або з можливістю переміщення, тому bind_{std::bind(f, std::forward<ConstrT>(args)...)}він все одно повинен працювати.
joshtch

4

Це питання було з C ++ 11 днів. Але для тих, хто знаходить його в результатах пошуку зараз, деякі оновлення:

std::tupleЧлен по - , як і раніше простий спосіб зберігати аргументи взагалі. ( std::bindРішення, подібне до рішення @ jogojapan, також буде працювати, якщо ви просто хочете викликати певну функцію, але не якщо ви хочете отримати доступ до аргументів іншими способами або передати аргументи більш ніж одній функції тощо).

У C ++ 14 і пізніших версіях, std::make_index_sequence<N>абоstd::index_sequence_for<Pack...> може замінити helper::gen_seq<N>інструмент, розглянутий у рішенні 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...>{});
    }
    // ...
};

У C ++ 17 і пізніших версіях std::applyможна використовувати для розпакування кортежу:

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

Ось повна програма C ++ 17, що демонструє спрощену реалізацію. Я також оновився, make_actionщоб уникнути посилальних типів у tuple, що завжди було погано для аргументів rvalue та досить ризиковано для аргументів lvalue.


3

Я думаю, у вас проблема XY. Навіщо намагатися зберігати пакет параметрів, коли ви просто можете використовувати лямбда на місці виклику? тобто

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

Оскільки в моєму додатку Action насправді має 3 різні функтори, які всі пов’язані, і я волів би, щоб класи, що містять її, містили 1 Action, а не 3 std :: function
Eric B
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.