具有不同签名的std :: function向量

22

我有若干个具有不同函数签名的回调函数。理想情况下,我希望将它们放入一个向量中,并根据某些条件调用适当的函数。

例如:

void func1(const std::string& value);

void func2(const std::string& value, int min, int max);

const std::vector<std::function<void(std::string)>> functions
{
    func1,
    func2,
};

我意识到上述内容是不可能的,但我想知道是否有任何替代方法值得考虑。目前我还没有找到合适的替代方案,虽然我已经尝试过使用std::bind,但没有达到我想要的效果。

这种事情可能吗?


3
为什么不给它们相同的签名呢?你不必在函数中使用所有参数。 - Ed Heal
4
为什么需要使用向量?向量只能包含相同类型的数据(可以有多态性)。如果你有不同签名的函数,你将无法使用for(auto&& function: functions){function();}等语句,所以你需要保持它们分开,或统一它们的签名(但如果它们确实具有不同的签名,则最后一种选项感觉像是把它们强行放进向量中...) - JBL
@JBL - 最终这就是我想做的。我猜你的意思是这不可能。 - ksl
1
你到底想做什么?看看这些不同的答案...我会投票关闭,因为“不清楚在问什么”。 - ikh
你已经用可能的解决方案来表达你的问题。你需要做的是陈述你实际想要实现什么。我认为在关闭投票之前还有时间这样做。 - quamrana
显示剩余3条评论
9个回答

27

您还没有说明在使用错误类型的向量存储func2后,希望能够做什么。

如果您事先知道参数,可以轻松使用std::bind将其放入向量中:

const std::vector<std::function<void(std::string)>> functions
{
    func1,
    std::bind(func2, std::placeholders::_1, 5, 6)
};

现在functions[1]("foo")将调用func2("foo", 5, 6),并且每次都会传递56func2

这里使用lambda表达式来实现相同的功能,而不是使用std::bind

const std::vector<std::function<void(std::string)>> functions
{
    func1,
    [=](const std::string& s){ func2(s, func2_arg1, func2_arg2); }
};

如果您还不知道参数,可以将引用绑定到某些变量:

int func2_arg1 = 5;
int func2_arg2 = 6;
const std::vector<std::function<void(std::string)>> functions
{
    func1,
    std::bind(func2, std::placeholders::_1, std::ref(func2_arg1), std::ref(func2_arg2))
};

现在functions[1]("foo")将调用func2("foo", func2_arg1, func2_arg2),您可以为整数赋新值以传递不同的参数到func2

使用一个lambda函数来代替std::bind

const std::vector<std::function<void(std::string)>> functions
{
    func1,
    [&](const std::string& s){ func2(s, func2_arg1, func2_arg2); }
};

虽然这种方式还算简单,但你需要在闭包或绑定表达式引用它们存在的时间内一直保留 int 变量,这看起来很不美观。


1
感谢您的回复。使用std::bind的方式,就像您展示的那样(因为我在编译时知道参数),这就是我尝试过的方法,当我说我尝试过std::bind但无法使其工作时。现在,由于您的回复,我已经可以做到了。 - ksl

7
您想要的可以通过多态实现。这个想法是创建一个具有特定签名的类,它在运行时会调用不同的方法。例如:
#include <iostream>
#include <functional>
#include <memory>
#include <vector>

void foo(int) {
    std::cout << "I'm foo!\n";
}

int bar(char, double) {
    std::cout << "I'm bar!\n";
}

class MyFunction {
    public:
        virtual ~MyFunction(){}

        virtual void operator()() = 0;
};

class MyFunctionA : public MyFunction {
    public:
        virtual void operator()() {
            foo(4);
        }
};

class MyFunctionB : public MyFunction {
    public:
        MyFunctionB(std::function<int(char,double)> f, char arg1, double arg2) : fun_(f), arg1_(arg1), arg2_(arg2) {} 

        virtual void operator()() {
            fun_(arg1_, arg2_);
        }
    private:
        std::function<int(char,double)> fun_;
        char arg1_;
        double arg2_;
};

int main() {
    using MyFunPtr = std::unique_ptr<MyFunction>;
    std::vector<MyFunPtr> v;

    v.emplace_back(new MyFunctionA());
    v.emplace_back(new MyFunctionB(bar, 'c', 3.4));

    for ( auto&& myfun : v ) {
        (*myfun)();
    }
    return 0;
}

你可以将派生类设计得尽可能复杂,但最终它们都具有相同的接口,因此你可以调用它们中的所有类。

4

对于C++ 17,可以使用std::variant来保存带有不同签名的std::function。在这种情况下,函数std::holds_alternative允许您在运行时区分它们:

示例:

#include <variant>
#include <iostream>
#include <functional>
#include <vector>


using FooInt = std::function<void(int)>;
using FooStr = std::function<void(std::string)>;

using FooVariant = std::variant<FooInt, FooStr>;

void foo(int a){
  std::cout << a << std::endl;
}

void bar(std::string a){
  std::cout << a << std::endl;
}

int main()
{
  std::vector<FooVariant> v;
  v.push_back(foo);
  v.push_back(bar);

  for(auto& f: v){
    if (std::holds_alternative<FooInt>(f))
      std::get<FooInt>(f)(1);
    else if (std::holds_alternative<FooStr>(f))
      std::get<FooStr>(f)("hello");
  }
}

1
直接回答你的问题是“不行”。任何运行时容器都只能让你存储相同类型的对象,而使用不同签名实例化的std::function<>将成为不同的数据类型。一般来说,你想要“一个具有不同签名函数的向量”的原因是当你有像下面这样的东西时(三步处理,其中输入接口是统一的(buffer& buf),输出接口是统一的on_event(Event evt)),但中间层是异构的process_...(...)
receive_message(buffer& buf)
  switch(msg_type(buf))
    case A: 
    case B:
    ...

process_A(A& a, One x, Two y)
  ...
  dispatch(Event evt);
  ...

process_B(B& b, Three x);
  ...
  dispatch(Event evt);
  ...

在不涉及元编程的解决方案中,通常会在初始化时预先准备一个函数对象来完成端到端,并将其存储在向量中。
vector <std::function<void(buffer& buf)>> handlers;

0
如果你有一个整型和一个字符串,你不能把它们放在一个向量中,但是你可以把它们放在一个结构体或者 std::tuple<> 中。对于两个函数类型也是一样的道理。

那不是问题 - BeyelerStudios
1
@BeyelerStudios:他明确要求使用替代vector<>来保存这两个函数。tuple<>难道不能胜任吗? - MSalters
通过“一些回调函数”,我理解在运行时数量是任意的,元组在这里行不通。 - BeyelerStudios
即使如此,集合很可能是相当有限的,您只需将不需要的函数设置为 nullptr 即可。 - MSalters

0

std::function 擦除了函数对象的确切类型,但保留了函数调用签名。如果您无法像Jonathan Wakely建议的那样提前绑定额外的参数,则可以使用 boost::variant< std::function<...>, std::function<...> > 作为您的向量成员。在调用站点上,您可以检查向量是否包含正确类型的函数对象,并相应地调用它。


1
谢谢。我已经成功使用std::bind实现了我想要的功能,感谢Jonathan Wakely的回答。 - ksl

0

正如JBL所提到的:如果您不知道它们的签名,您将如何称呼它们?

考虑将您的min, max 参数转换为一种带有某些基类Parameter的参数类型,回调签名将是void(const std :: string&amp;,const Parameter&amp;)void(const std :: string&amp;,const Parameter *),以防您希望 nullptr 表明没有其他参数。现在,您的回调将需要检查是否给出了正确的参数(如果有)。可以通过使用访问者,typeid或枚举来完成。所有这些都有利有弊。

您将如何决定要调用哪个回调?我认为您应该将C样式的回调转换为处理程序对象,它们可能实现bool canHandle(const Parameter&)函数以测试处理程序是否适用于所呈现的参数。

Jonathan Wakely和Svalorzen提出了他们的方法,其中参数和函数是同一对象(1对1关系)。在此示例中,它们是分开的(如果您具有多对多关系):

#include <cassert>
#include <string>
#include <typeinfo>
#include <vector>

class ParameterBase {
public:
    ParameterBase(const std::string& value) : m_value(value) { }
    virtual ~ParameterBase() { }
    const std::string& GetValue() const { return m_value; }
private:
    std::string m_value;
};

class HandlerBase {
public:
    virtual bool CanHandle(const ParameterBase& params) const = 0;
    virtual void Handle(const ParameterBase& params) = 0;
};

class Handler1 : public HandlerBase {
public:
     class Parameter : public ParameterBase {
     public:
          Parameter(const std::string& value) : ParameterBase(value) { }
          ~Parameter() { }
     };

     bool CanHandle(const ParameterBase& params) const { return typeid(Parameter) == typeid(params); }
     void Handle(const ParameterBase& params) {
          assert(CanHandle(params));
          const Parameter& p = static_cast<const Parameter&>(params);
          // implement callback1
     }
};

void foo(const std::vector<HandlerBase*>& handlers) {
     Handler1::Parameter params("value");
     for(auto handler : handlers)
         if(handler->CanHandle(params)) {
             handler->Handle(params);
             // no break: all handlers may handle params
             // with break: only first handler (if any) handles params
         }
}

你会怎么称呼它们?例如访问者模式。 - MSalters

0

不确定这对您有多大用处,它基于boost::any,冗余参数将被忽略。您可以添加try...catch来捕获boost::bad_any_cast,以防止参数和参数类型不匹配导致崩溃。虽然我认为常规的std::bind是更好的选择。

演示

#include <boost/any.hpp>
#include <functional>
#include <vector>
#include <cstddef>
#include <memory>
#include <tuple>
#include <utility>
#include <iostream>
#include <string>

struct IGenericFunction
{
    virtual ~IGenericFunction() = default;

    virtual void call(boost::any a1 = boost::any{}
                    , boost::any a2 = boost::any{}
                    , boost::any a3 = boost::any{}
                    , boost::any a4 = boost::any{}) = 0;
};

template <typename... Args>
class GenericFunction : public IGenericFunction
{
public:
    GenericFunction(std::function<void(Args...)> f) : _f{ f } {}

    virtual void call(boost::any a1 = boost::any{}
                    , boost::any a2 = boost::any{}
                    , boost::any a3 = boost::any{}
                    , boost::any a4 = boost::any{}) override
    {
        call_func(std::make_tuple(a1, a2, a3, a4)
                , std::make_index_sequence<sizeof...(Args)>{});
    }

private:            
    template <typename Tuple, std::size_t... Indices>
    void call_func(Tuple t, std::index_sequence<Indices...> s)
    {
        _f(boost::any_cast<
                typename std::tuple_element<Indices, Params>::type
           >(std::get<Indices>(t))...);
    }

    std::function<void(Args...)> _f;

    using Params = std::tuple<Args...>;
};

template <typename... Args>
std::shared_ptr<IGenericFunction> make_generic_function_ptr(void(*f)(Args...))
{
    return std::make_shared<GenericFunction<Args...>>(f);
}

void func1(const std::string& value)
{
    std::cout << "func1 " << value << std::endl;
}

void func2(const std::string& value, int min, int max)
{
    std::cout << "func2 " << value << " " << min << " " << max << std::endl;
}

int main()
{
    std::vector<std::shared_ptr<IGenericFunction>> functions;

    functions.push_back(make_generic_function_ptr(&func1));    
    functions.push_back(make_generic_function_ptr(&func2));

    for (auto f : functions)
    {
        f->call(std::string("abc"), 1, 2);
    }
}

0

我尝试使用函数指针,并将std::function<int(int)>*强制转换为void*,它可以成功编译,但有时会导致分段错误:

int Fun(int a)
{
    std::cout << a << std::endl;
    return ++(++a);
}

int main()
{
    std::function<int(int)> originPFun = Fun;
    void *ppFun;
    // ppFun = (void*)&Fun; // This way will cause segmentation fault
    ppFun = (void*)&originPFun; // This way can run seuccessful and get right result
    std::function<int(int)> *pFun = (std::function<int(int)>*)(ppFun);
    std::cout << (*pFun)(5) << std::endl;
    system("pause");
    return 0;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接