如何在运行时恢复函数指针的类型

4
在代码中,我在一个管理类中注册了一个或多个函数指针。在这个类中,我有一个映射表,将函数的参数类型映射到该函数。它可能看起来像这样:std::map< std::vector<std::type_index> , void*>
template<typename Ret, typename... Args>
void Register(Ret(*function)(Args...)) {
    void* v = (void*)function;
    // recursively build type vector and add to the map
}

在运行时,代码会接收来自外部脚本的调用,并带有任意数量的参数。这些参数可以被读取为原始数据类型或作为在编译时指定的自定义类型。


每次从脚本中调用时,我都必须找出要调用哪个函数,然后调用它。前者很容易解决(在循环中使用type_index填充向量),但是我想不到后者的解决方案。
我的第一种方法是使用可变参数模板进行递归,并为每个读取类型添加一个模板参数-但是这被证明是不可能的,因为模板是在编译时构造的,而任意数量的参数是在运行时读取的。
但是如果没有可变参数模板,我看不到任何实现这一点的可能性。我考虑使用boost::any代替void*,但我没有看到它如何解决需要强制转换回原始类型的问题。我也考虑过使用std::function,但那将是一个带有模板的类型,因此无法存储具有不同参数的函数的映射。

如果我的问题不清楚,请考虑LuaBinds支持注册重载函数的可能性。我试图理解它是如何实现的(没有可变参数模板,没有C++11),但是没有成功。


也许?http://codereview.stackexchange.com/questions/46473/dynamically-call-lambda-based-on-stream-input-try-3 不确定是否相似。我认为你可能正在编写解释器……不过也不确定。 - Brandon
顺带提一下,POSIX需要在void*和函数指针之间进行转换,但标准的C和C++不支持这样做。 - Cheers and hth. - Alf
@Cheersandhth.-Alf 感谢您的建议,我之前并不知道这一点,尽管它编译没有错误。类似的(符合规范)方法是存储指向模板类的虚基类的指针,该模板类存储实际的函数指针,但这将导致相同的问题,需要进行相同的转换。我并不完全确定 void*/转换解决方案!这只是我能想到的。 - Appleshell
1
我认为你正在寻找类型擦除。据我所知,这里最简单的实现方法是使用一个包装器函数模板,它将实际类型存储在其模板参数中:template<class T> void wrapper(void* p) { static_cast<T*>(p)->whatever(); } void(*f)(void*) = wrapper<MyFirstType>; f = wrapper<MySecondType>;等等。 - dyp
你并没有复制 std::functionstd::bind 的功能吗? - Steve Lorimer
@SteveLorimer 我不想替换任何已经存在的内容,我只是试图编写脚本调用的处理程序。使用 std::bindstd::function 的解决方案会很棒!我之前没有考虑过 std::bind,因为在我做的研究中没有提到过它。 - Appleshell
3个回答

3
假设您有某种类型的参数存储在一个vector中,并且已知一个完整的函数。

您可以调用此函数。将执行此操作的函数称为invoke

接下来,找出如何对template<class... Args>执行此操作。增强invoke

因此,您编写了:

typedef std::vector<run_time_stuff> run_time_args;
template<class... Args>
void invoke( void(*func)(Args...), run_time_args rta )

此时,需要注意我们已经知道了参数的类型。我不会声称上述内容很容易编写,但我相信你可以弄清楚。

现在我们总结一下:

template<class...Args>
std::function<void(run_time_args)> make_invoker(void(*func)(Args...)){
  return [func](run_time_args rta){
    invoke(func, rta);
  };
}

现在,您存储的不再是void*指针,而是std::function类型的调用者。当您将函数指针添加到该机制时,使用make_invoker而不是强制转换为void*。
基本上,在我们拥有类型信息的时候,我们存储如何使用它。然后,在我们想要使用它的地方,我们使用存储的代码!
编写invoke是另一个问题。它可能涉及索引技巧。
假设我们支持两种参数--double和int。运行时的参数然后被加载到std :: vector>中作为我们的run_time_args。
接下来,让我们扩展上述的invoke函数以在参数类型不匹配的情况下返回错误。
enum class invoke_result {
  everything_ok,
  error_parameter_count_mismatch,
  parameter_type_mismatch,
};
typedef boost::variant<int,double> c;
typedef std::vector<run_time_stuff> run_time_args;
template<class... Args>
invoke_result invoke( void(*func)(Args...), run_time_args rta );

现在是关于索引技巧的一些样板代码:

template<unsigned...Is>struct indexes{typedef indexes type;};
template<unsigned Max,unsigned...Is>struct make_indexes:make_indexes<Max-1, Max-1,Is...>{};
template<unsigned...Is>struct make_indexes<0,Is...>:indexes<Is...>{};
template<unsigned Max>using make_indexes_t=typename make_indexes<Max>::type;

有了这个,我们可以编写一个调用器:

namespace helpers{
  template<unsigned...Is, class... Args>
  invoke_result invoke( indexes<Is...>, void(*func)(Args...), run_time_args rta ) {
    typedef void* pvoid;
    if (rta.size() < sizeof...(Is))
      return invoke_result::error_parameter_count_mismatch;
    pvoid check_array[] = { ((void*)boost::get<Args>( rta[Is] ))... };
    for( pvoid p : check_array )
      if (!p)
        return invoke_result::error_parameter_type_mismatch;
    func( (*boost::get<Args>(rts[Is]))... );
  }
}

template<class... Args>
invoke_result invoke( void(*func)(Args...), run_time_args rta ) {
  return helpers::invoke( make_indexes_t< sizeof...(Args) >{}, func, rta );
}

只有当func的参数与run_time_args中传入的参数完全匹配时,才能正常工作。

请注意,我在未使用std::move操作std::vector的情况下进行了快速操作。而且上述内容不支持隐式类型转换。另外,我没有编译任何上面的代码,所以它可能充满了拼写错误。


我不确定我是否正确理解了代码,但是这不需要持有一个模板类型吗?我该如何以这种方式在映射中存储具有不同签名的函数? - Appleshell
@AdamS,您在映射中存储std::function。这个std::function的签名始终相同。我们创建std::function以便它存储函数指针,并知道如何将“通用”参数的std::vector解包到函数调用中。因为我们在创建std::function时知道函数指针的签名,所以我们可以编写该代码。我试图引导您完成以下步骤-您能否编写invoke函数?也就是说,您能否在不查找的情况下调用一个固定函数?然后,您能否将其作为该函数类型的template编写? - Yakk - Adam Nevraumont
@Yakk 没有名为foo的函数 - 调用将映射到静态分派函数。该脚本中每个函数都使用相同的分派函数。foo() 映射到 dispatch(lua_State*)。就像 bar() 一样。无论参数是什么。但是,每个函数都保存了指向类实例的指针,foobar 不同的实例,但在每个实例中,我需要有一个将不同签名映射到不同函数的映射表。所以 foo(int, int) 分派到与 foo(int, bool) 相同的实例,并使用我已经给出的代码接收参数。(续) - Appleshell
1
@AdamS http://ideone.com/B8fRw1 是使用存根函数(可能有错误的索引等)的“invoke”。请使用“typedef lua_state* run_time_args”语句。在您的映射中存储“std :: function <void(run_time_args)>”(而不是您的“void *”)。使用上面编写的“make_invoker”将函数指针转换为该类型。我漏掉了什么吗? - Yakk - Adam Nevraumont
你很棒。我会因为你的耐心给你额外的声望。 - Appleshell
显示剩余12条评论

1

我几周前试着使用可变参数模板,并想出了一种解决方案,可能会对你有所帮助。

DELEGATE.H

template <typename ReturnType, typename ...Args>
class BaseDelegate
{
public:
    BaseDelegate()
        : m_delegate(nullptr)
    {

    }

    virtual ReturnType Call(Args... args) = 0;
    BaseDelegate* m_delegate;
};

template <typename ReturnType = void, typename ...Args>
class Delegate : public BaseDelegate<ReturnType, Args...>
{
public:
    template <typename ClassType>
    class Callee : public BaseDelegate
    {
    public:
        typedef ReturnType (ClassType::*FncPtr)(Args...);
    public:
        Callee(ClassType* type, FncPtr function)
            : m_type(type)
            , m_function(function)
        {

        }

        ~Callee()
        {

        }

        ReturnType Call(Args... args)
        {
            return (m_type->*m_function)(args...);
        }

    protected:
        ClassType* m_type;
        FncPtr m_function;
    };

public:
    template<typename T>
    void RegisterCallback(T* type, ReturnType (T::*function)(Args...))
    {
        m_delegate = new Callee<T>(type, function);
    }

    ReturnType Call(Args... args)
    {
        return m_delegate->Call(args...);
    }
};

MAIN.CPP

class Foo
{
public:
    int Method(int iVal)
    {
        return iVal * 2;
    }
};

int main(int argc, const char* args)
{
    Foo foo;
    typedef Delegate<int, int> MyDelegate;

    MyDelegate m_delegate;
    m_delegate.RegisterCallback(&foo, &Foo::Method);

    int retVal = m_delegate.Call(10);

    return 0;
}

2
这如何解决OP的多重签名问题? - Yakk - Adam Nevraumont

0

不确定你的需求是否允许,但你可能可以使用std::functionstd::bind

以下解决方案做出了以下假设:

  • 您知道要调用的函数及其参数
  • 函数可以具有任何签名和任意数量的参数
  • 您想要使用类型擦除来存储这些函数和参数,并在以后的某个时间点调用它们

这是一个可行的示例:

#include <iostream>
#include <functional>
#include <list>

// list of all bound functions
std::list<std::function<void()>> funcs;

// add a function and its arguments to the list
template<typename Ret, typename... Args, typename... UArgs>
void Register(Ret(*Func)(Args...), UArgs... args)
{
    funcs.push_back(std::bind(Func, args...));
}

// call all the bound functions
void CallAll()
{
    for (auto& f : funcs)
        f();
}

////////////////////////////
// some example functions
////////////////////////////

void foo(int i, double d)
{
    std::cout << __func__ << "(" << i << ", " << d << ")" << std::endl;
}

void bar(int i, double d, char c, std::string s)
{
    std::cout << __func__ << "(" << i << ", " << d << ", " << c << ", " << s << ")" << std::endl;
}

int main()
{
    Register(&foo, 1, 2);
    Register(&bar, 7, 3.14, 'c', "Hello world");

    CallAll();
}

这看起来不错,bind 似乎是朝着正确方向迈出的一步。然而,在编译时只知道参数类型,而不知道参数本身 - 它们在运行时给出(函数可以多次调用,使用不同的参数),因此它们不能在 Register 中绑定。 - Appleshell
@AdamS - 所有函数的签名都会相同吗?(即:参数数量相同,参数类型相同) - Steve Lorimer
不,这就是我面临的问题。可以注册任意数量具有不同签名的函数。脚本将调用 function(100, 10, 'foo'),然后我必须调用其中一个函数(符合签名的函数)。请参见我原始问题中的链接,以获取一个很好的示例。 - Appleshell

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