Как хранить аргументы вариативного шаблона?


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 01

Ответы:


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


Разве привязка rvalues ​​не опасна? Разве они не станут недействительными, если они 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> (арг) ...))
затмил

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.