具有不同签名的std::函数的矢量

Vector of std::function with different signatures

本文关键字:std 函数      更新时间:2023-10-16

我有许多具有不同签名的回调函数。理想情况下,我想把这些放在一个向量中,并根据某些条件调用合适的向量。

例如

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,但没能实现我想要的。

这样的事情可能吗?

您还没有说明在将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),并且每次都会将56传递给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 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变量的可调用对象(闭包或绑定表达式)存在,就需要保留这些变量。

通过polymorphism可以实现您想要的。其想法是创建一个具有特定签名的类,该类在运行时将调用不同的方法。例如:

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

您可以根据需要使派生类变得复杂,但由于最终它们都具有相同的接口,因此您可以调用所有派生类。

对于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");
  }
}

问题的直接答案是"否"。任何运行时容器都只允许存储相同类型和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;

如果你有一个int和一个字符串,你不能把它们放在一个向量中,但你可以把它们放进一个结构或std::tuple<>中。这同样适用于两种函数类型。

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

不确定这对您有多有用,它基于boost::any,冗余参数被忽略。您可以为boost::bad_any_cast添加try...catch,以防止在参数和参数类型不匹配的情况下崩溃。尽管我认为常规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);
    }
}

正如JBL所提到的:如果你不知道他们的签名,你会怎么称呼他们?

考虑将min, max参数转换为具有某个基类Parameter的参数类型,如果您希望nullptr不指示其他参数,则回调签名将为void(const std::string&, const Parameter&)void(const std::string&, const Parameter*)。现在,您的回调将需要检查是否为它们提供了正确的参数(如果有的话)。这可以通过使用访问者、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
         }
}

我尝试使用函数指针,并强制执行std::function<int(int)>*要作废*,可以成功编译,但有时会导致分段错误:

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