如何存储可变参数模板参数?


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 2013年

这回答了你的问题了吗?C ++如何将参数包存储为变量
user202729

Answers:


67

为了完成您想在这里完成的工作,您必须将模板参数存储在元组中:

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

此外,您必须稍微改变构造函数。特别是,使用进行初始化argsstd::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 2014年

1
我认为@jogojapan提供了一种更简洁易读的解决方案。
蒂姆·柯伊伯

1
@Riddick更改args(std::make_tuple(std::forward<Args>(args)...))args(std::forward<Args>(args)...)。顺便说一句,我是很久以前写的,我不会使用此代码来将函数绑定到某些参数。我会用std::invoke()std::apply()现在。
0x499602D2 '19

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将它们包装在引用对象中。仅传递aT &将创建值的副本,而不是引用。

Coliru上的操作代码


绑定右值不是很危险吗?当add在不同的范围内定义而不是在哪里定义时,它们不会失效act()吗?构造函数不应该得到ConstrT&... args而不是ConstrT&&... args
蒂姆·柯伊伯

1
@Angelorf对不起,我迟到了。您的意思是呼叫的右值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...>可以替换0x499602D2解决方案helper::gen_seq<N>所示的工具:

#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问题。当您只可以在调用站点上使用lambda时,为什么还要麻烦地存储参数包?即

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

因为在我的应用程序中,一个动作实际上有3个不同的函子都相关,所以我宁愿包含它的类包含1个动作,而不是3个std :: function
Eric B
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.