【问题标题】:C++ generic function call with varargs parameter带有可变参数的 C++ 泛型函数调用
【发布时间】:2023-03-03 00:25:02
【问题描述】:

在我的项目中,我有不同类型的输入参数数量不同的函数。由于这些函数是库的一部分,因此我无法更改它们的定义或主体。

void methodA(boolean p1, int p2, long p3){
    ... some unrelevant code here ...
}

void methodB(int p1, int p2, int p3, long p4){
    ... some unrelevant code here too ...
}

int methodC(long p4){
    ...
}

在我的项目中,我需要一个方法,它可以接收其中一个函数的地址。此外,它接收格式良好的参数列表(适合第一个参数中的函数)。那么这个方法就得用传递的参数调用传递的函数。

这是我现在所拥有的:(我已经稍微简化了代码以使我的想法清晰)

void intercaller(void* some_func_address, ...){

    // VARARGS parameters extractor
    va_list listPointer;
    va_start( listPointer, some_func_address );

    int p1 = va_arg( listPointer, int );
    int p2 = va_arg( listPointer, int );
    int p3 = va_arg( listPointer, int );
    long p4 = va_arg( listPointer, long );

    // TODO: THIS IS NOT GENERIC CALL , CANN ONLY CALL METHOD B
    ((void (*)( int , int , int , long )) some_func_address)( p1 , p2 , p3 , p4 );

    va_end( listPointer );
}

我的问题是实际的函数调用。函数调用中的参数列表应该是通用的,并且应该能够包含不同数量的参数,遗憾的是我不知道该怎么做......我试过像这里一样传递可变参数列表:

((void (*)( va_list )) some_func_address)( listPointer);

但这会弄乱被调用函数中的参数...

所以我的问题是:有没有办法以通用方式调用具有给定参数的给定函数?也许我需要某种 typedeff 或包装函数?

【问题讨论】:

  • 你可能想要std::invoke。如果你的编译器还不支持它,你可以在别处找到它的实现。
  • 由于您明确需要 C++ 解决方案,我已从您的标签中删除 [c]。 C 是一门独立的语言。
  • 你对签名void intercaller(void* some_func_address, ...)的单个函数有硬性要求吗?
  • 你不需要可变参数函数,你需要可变参数模板和/或 lambdas。

标签: c++ generics typedef


【解决方案1】:

va_args 对我来说仍然有点黑魔法,但我相信 va_start 的第二个 arg 应该是被调用函数的第一个 arg。我不明白你的“clazz”是什么。我相信您应该将 va_start 称为:

va_start( listpointer, some_func_address ); 

代替:

va_start( listPointer, clazz );

【讨论】:

  • 你是对的 :) 忘记更改以简化...已编辑
  • 为了正确起见:va_start 的第二个参数是可变参数之前的最后一个命名参数...
【解决方案2】:

如果您还没有std::invoke,请使用variadic templates。要很好地处理void 函数,请使用SFINAE

template<typename R, typename... Args>
auto call(R(*function)(Args...), Args... args) -> typename std::enable_if<!std::is_same<R, void>::value, R>::type {
    return function(args...);
}

template<typename... Args>
void call(void (*function)(Args...), Args... args) {
    function(args...);
}

例子:

void a() {
    std::cout << 'a';
}

void b(int a) {
    std::cout << "b:" << a;
}

int c(int a) {
    return a;
}

int main() {
    call(a);
    call(b, 1);
    std::cout << "c:" << call(c, 2);
}

不要忘记#include &lt;type_traits&gt;std::enable_ifstd::is_same

Try it online!

【讨论】:

  • @AksimElnik 我添加了一个链接以在线尝试此解决方案。如果您仍然遇到问题,您能告诉我您的编译器、版本以及您传递给它的参数吗?
  • 有趣的解决方案。有没有办法以某种方式定义指向调用模板的函数指针?这样的事情不会起作用void* func_ptr = &amp;call;,如果拦截器函数需要在某处注册为回调函数,我想它不会起作用。
  • @HaasipSatang 你是对的,它不会工作。因为这是一个函数模板(而不是真正的函数,在模板实例化之前),你不能获取它的地址。如果在注册回调时可以知道函数类型,则可以简单地实例化模板,然后获取实例化函数的地址。但是,在回调的情况下,简单地使用(可能是通用的)lambda 通常会好得多。
  • @CássioRenan 不确定我是否理解。上面的模板如何在不指定具体类型的情况下以通用方式实例化?假设您的示例中的所有函数(a、b 和 c)都将在某处注册(例如在 JNI 中注册本机时)。然后你想通过调用函数模板来拦截调用(假设这将有办法找出目标方法而不将其作为输入参数接收)。你能在你的例子中勾勒出你将如何为这种通用案例实例化模板吗?
  • @HaasipSatang 如果你正在接受void*(这意味着你正在执行类型擦除),并且你正在运行时调度调用,所以不,这是不可能的。我的意思是“如果您在注册回调时能够知道函数类型”,这可能仅在您在编译时注册所有回调时才有效,然后您可以实例化您的@987654333 @,假设它是我建议的模板。
【解决方案3】:

这对你有帮助吗?

#include <stdarg.h>

template <typename T>
T extract(va_list& list)
{
    return va_arg(list, T);
}

template<typename Result, typename ... Parameters>
Result call(Result(*function)(Parameters...), va_list& list)
{
    return function(extract<Parameters>(list)...);
}

void f1(int x, int y)
{
    std::cout << x << ' ' << y << std::endl;
}

void f2(double x, double y)
{
    std::cout << x << ' ' << y << std::endl;
}

void interceptor(void* f, ...)
{
    va_list list;
    va_start(list, f);
    if(f == &f1)
    {
        call(f1, list);
    }
    else if(f == f2)
    {
        call(f2, list);
    }
    va_end(list);
}

int main(int argc, char* argv[])
{
    interceptor((void*)&f1, 7, 7);
    interceptor((void*)&f2, 10.12, 12.10);
    return 0;
}

我个人更喜欢将代表函数的枚举传递给拦截器函数而不是 void* 指针并在内部使用 switch/case。

如果你可以让拦截器成为一个模板函数,它会变得更加容易(完全放弃call模板函数):

template<typename Result, typename ... Parameters>
void interceptor(Result(*function)(Parameters...), ...)
{
    va_list list;
    va_start(list, function);
    function(extract<Parameters>(list)...);
    va_end(list);
}

int main(int argc, char* argv[])
{
    interceptor(&f1, 7, 7);
    interceptor(&f2, 10.12, 12.10);
    return 0;
}

【讨论】:

  • 首先感谢您的帮助:) 但是我有一个问题。为什么在上面的示例中,您调用 f1 或 f2 而不仅仅是 f?调用 void* f 会不会更容易,因为您直接将其作为参数获取?我现在要做的是call(f, list),但我得到了一个错误的参数类型错误......我不能将拦截器作为模板函数,因为我需要在真正执行它之前拥有它的地址......而且就我而言知道函数指针的地址很难得到......
  • 嗯,这就是问题所在:f 只是一个 void* 指针 - 无法从中推导模板参数!所以没有其他办法。实际上,它甚至不是 legal C/C++ 将函数指针强制转换为 void*...
【解决方案4】:

现在来自你的另一个question,这个呢:

(旁注:引用的问题告诉(在 cmets 中) void* 指针来自一些自定义映射,因此据我所知,将它们替换为其他不应该有任何问题适当的指针/类——我将要做的……)

#include <stdarg.h>

class FunctionWrapper
{
public:
    virtual ~FunctionWrapper() { }
    virtual void operator()(va_list&) = 0;
};

template<typename Result, typename ... Parameters>
class FWrapper : public FunctionWrapper
{
    Result (*mFunction)(Parameters...);
    template <typename T>
    T extract(va_list& list)
    {
        return va_arg(list, T);
    }
public:
    FWrapper(Result (*function)(Parameters...))
            : mFunction(function)
    { }
    virtual void operator()(va_list& list)
    {
        static_cast<void>(mFunction(extract<Parameters>(list)...));
    }
};

// facilitates creating the wrappers:
template<typename Result, typename ... Parameters>
FunctionWrapper* createWrapper(Result (*function)(Parameters...))
{
    return new FWrapper<Result, Parameters ...>(function);
}

void f1(int x, int y)
{
    std::cout << x << ' ' << y << std::endl;
}

void f2(double x, double y)
{
    std::cout << x << ' ' << y << std::endl;
}

// e. g.:
FunctionWrapper* gWrappers[] = { createWrapper(&f1), createWrapper(&f2) };
// from your other question: you'd fill the wrappers into the map you mentioned there:
// map[whatever] = createWrapper(&function);

void interceptor(FunctionWrapper* wrapper, ...)
{
    va_list list;
    va_start(list, wrapper);
    (*wrapper)(list);
    va_end(list);
}

int main(int argc, char* argv[])
{
    interceptor(gWrappers[0], 7, 7);
    interceptor(gWrappers[1], 10.12, 12.10);

    return 0;
}

这通过多态性解决了这个问题:一个函数包装类模板类(我们需要一个非模板基类,以便能够将所有模板实例放入数组或映射中;这是你原来的——但实际上是非法的– void* 提供的指针),将 va_list 解析为参数并调用原始函数...

【讨论】:

  • 再次感谢您的努力。不幸的是,在编译期间,我的 f1 和 f2 函数的类型是未知的,这意味着我只得到指向它们的 void* 句柄,因此创建包装器的调用无法编译:/
  • 呃 - 这是一个 非常 重要的细节丢失......关于签名 - 至少在编译时这些是已知的,或者你是否也动态获取此信息?
  • 是的,我也在运行时获取它们。
  • 现在我想知道 - 你从哪里获得这些功能?如果在编译时没有可用的签名,它们打算如何从普通 C 或 C++ 调用(让我们暂时跳过 JNI 部分,假设您想从 C/C++ 正常使用这些函数)?他们打算如何动态地提供参数?
  • DLL,好吧,Windows,我假设 如果我理解正确,LoadLibrary 和 GetProcAddress 的东西(如 here 的解释)是你无法控制的,你可能甚至不知道实际加载了哪个 DLL。到目前为止是正确的吗?使用普通的 C 或 C++ 会变得很困难。尝试了 void(*)(...) 函数指针,但没有让它工作......
猜你喜欢
  • 2021-11-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多