我该如何创建一个包含不同原型的函数指针数组?

28

我定义了几个函数,如下所示:

ParentClass*    fun1();
ParentClass*    fun2();
ParentClass*    fun3(bool inp=false);
ChildClass*     fun4();
ChildClass*     fun5(int a=1, int b=3);

我希望将它们放入某种数组中,如下所示:

void* (*arr[5])() = {
    (void* (*)())fun1,
    (void* (*)())fun2,
    (void* (*)())fun3,
    (void* (*)())fun4,
    (void* (*)())fun5
}

现在我想简单地使用这个函数数组:

for(int i=0; i<5; i++)
    someFunction(arr[i]());

现在我意识到问题出在void* (*arr[5])(),但考虑到我只想使用这些函数而不提供参数,我希望所有这些函数都成为同一个数组的一部分。

但是,这些方法非常C-style。是否有更好的方法可以在C ++中使用模板来实现?


你为什么想要从返回值中丢弃类型?ChildClass没有继承ParentClass吗? - Caleth
当我使用它时,我实际上将它转换为ParentClass。 我可以将其更改为ParentClass*而不是void *,但这样我就必须将数组中的每个条目强制转换。 相反,我当前选择在从数组中检索它之后进行转换。 - vc669
我在问题中传达错误,其实我想使用它。已经修改了问题。 - vc669
在你的第一个片段中,你声明了 fun4 两次,这是打字错误吗? - Fabio says Reinstate Monica
是的。已更正。 - vc669
5个回答

42

无论是否使用C风格,您现在面临的是明显的未定义行为。请使用Lambda表达式:

void (*arr[5])() = {
    [] { fun1(); },
    [] { fun2(); },
    [] { fun3(); },
    [] { fun4(); },
    [] { fun5(); }
};

这些是可以的,因为它们通过函数的正确类型执行调用,并且本身可转换为void (*)()

由于Lambda提供了转换的上下文,因此转发返回的值仍然足够简单。在您的情况下,由于ChildClass据说继承自ParentClass,隐式转换就足够了:

ParentClass *(*arr[5])() = {
    []() -> ParentClass * { return fun1(); },
    []() -> ParentClass * { return fun2(); },
    []() -> ParentClass * { return fun3(); },
    []() -> ParentClass * { return fun4(); },
    []() -> ParentClass * { return fun5(); }
};

这是否使函数的返回类型为 void - vc669
@vc669 是的,因为你根本不使用它。但是,你也可以使用lambda来协调返回类型,我猜ChildClass继承自ParentClass,所以只需[]() -> ParentClasss * { return fun1(); } - Quentin
没有意识到我的使用方式(或缺乏使用)会有所不同。我实际上确实打算使用它。我已经更新了问题,根据你的评论解决了问题。 - vc669
1
@Aconcagua 是的,但要保持一致性。可以通过编写一个函数来避免重复,该函数一次性接收所有 fun* 并返回包装函数的数组。 - Quentin
出于好奇,如果不需要中间函数调用(lambda),一个好的编译器会优化它并将fun1实际放入数组中吗? - Aconcagua
1
@Aconcagua 几乎:它会被编译成单个 jmp。如果适用,函数体将被内联到 lambda 中。 - Quentin

16

但是,考虑到我只想在不提供参数的情况下使用这些函数,这样行不通。你是否曾经想过,当你把函数声明放在头文件中时,为什么必须在头文件中编写默认参数而不能将其放在实现源文件的定义中?

那是因为默认参数实际上并没有“嵌入”到函数中,而是由编译器在调用位置增加这些参数以补充函数调用,在省略这些参数的地方。(编辑:正如@Aconcagua在评论中所观察到的那样,由于默认参数通常作为头函数声明的一部分定义,任何更改默认值的更改都需要对包含这些头文件的任何编译单元进行完全重新编译,才能真正生效!)

虽然可以通过做一些非常奇怪的类型转换来构造一个函数指针数组,但最终你将不得不回到原始函数调用签名,以避免调用未定义的行为。

如果要绑定函数指针和一组默认参数,则必须将它们绑定在某种抽象调用的类型中,并提供多态接口供外部使用。因此,您可以使用std::vector<function_binder>function_binder[],其中函数绑定器具有调用函数的operator()

但是,在进行绑定时,您可以在匿名函数(即lambda)中进行绑定。在lambda实例化时,会绑定默认参数。

std::vector<void(*)()> fvec = {
    []{ func0(); },
    []{ func1(); },
    []{ func2(); }
};

值得一提的是,由于参数是在头文件中提供的,如果更改了这些参数,则依赖于它们的任何翻译单元也需要重新编译以使这些更改生效。 - Aconcagua

10
你可以使用 std::bind
std::function<ParentClass *(void)> arr[5] = {
    std::bind(&fun1),
    std::bind(&fun2),
    std::bind(&fun3, false),
    std::bind(&fun4),
    std::bind(&fun5, 1, 3)
};

现在你可以做

for(int i=0; i<5; i++)
    arr[i]();

你必须确保所有函数的所有参数都是绑定过的。

这对成员函数也适用。你只需要将对象引用(例如this)作为第一个参数进行绑定即可。


1
自从C++11及以上版本,使用bind已经没有意义了,因为lambda更方便且更快。 - Marek R
@Marek R - 根据实现,std::bind 可能只返回一个 lambda,因为它的返回类型是未指定的。你使用哪个可能只是品味问题。 - Detonar
2
我的口味是 std::bind,因为它可以让纪律性不太好的开发者编写小函数。Lambda 经常被过度使用并且变得非常庞大,但它仍然更易于使用且更快。std::bind 会创建复杂的模板,这在分析崩溃日志时非常麻烦。 - Marek R
@Detonar 现在这是因为 std::function 对于这个任务来说过于复杂了 :p - Quentin
我不认为std::function有什么特别的面向对象之处,而且它也不再具备类型安全性--可以说它的类型安全性更弱了,因为其构造函数可以在参数和返回类型方面进行一些转换。 - Quentin
显示剩余3条评论

4

一个的解决方案:

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  -> decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }


template<auto f, class R, class...Args>
struct explicit_function_caster {
  using Sig=R(Args...);
  using pSig=Sig*;
  constexpr operator pSig()const {
    return [](Args...args)->R {
      return static_cast<R>(f(std::forward<Args>(args)...));
    };
  }
};

template<auto f>
struct overload_storer_t {
  template<class R, class...Args>
  constexpr (*operator R() const)(Args...) const {
    return explicit_function_caster<f, R, Args...>{};
  }
  template<class...Args>
  auto operator()(Args&&...args)
  RETURNS( f( std::forward<Args>(args)... ) )
};
template<auto f>
overload_storer_t<f> generate_overloads={};

#define OVERLOADS_OF(...) \
  generate_overloads< \
    [](auto&&...args) \
    RETURNS( __VA_ARGS__( decltype(args)(args)... ) ) \
  >

这是很多样板文件,但可以得到:

ParentClass* (*arr[5])() = {
  OVERLOADS_OF(fun1),
  OVERLOADS_OF(fun2),
  OVERLOADS_OF(fun3),
  OVERLOADS_OF(fun4),
  OVERLOADS_OF(fun5)
};
void (*arr2[5])() = {
  OVERLOADS_OF(fun1),
  OVERLOADS_OF(fun2),
  OVERLOADS_OF(fun3),
  OVERLOADS_OF(fun4),
  OVERLOADS_OF(fun5)
};

基本上,generate_overloads<x> 接受一个 constexpr 可调用对象 x,并允许您在编译时将其转换为任何兼容签名的函数指针,并使用(几乎)任何签名进行调用。
与此同时,OVERLOADS_OF 将函数名转换为一个 constexpr 对象,在该函数名上执行重载决议。我在这里使用它,因为作为函数指针的 fun3 不知道其默认参数,但在重载决议时它会知道。
在这种特定情况下,编写玩具 lambda 表达式来完成这项工作要容易得多;这只是尝试自动编写那些任意兼容签名的玩具 lambda 表达式。

constexpr (*operator R() const)(Args...) const 喔,这是什么鬼... 另外,decltype(args)(args)...std::forward 的替代品还是还有其他微妙之处? - Quentin
@Quentin 是的。我没有 args 的类型。强制转换运算符是因为 C++2a 编译器不喜欢基于 auto 的部分特化,所以我不能轻松地解包 Sig:这是一种类型推导的转换运算符,用于函数指针。 - Yakk - Adam Nevraumont
1
@Quentin Laugh,我想我在一个const方法中转换为了一个const函数指针。不小心的是:函数指针是const这一事实是无意义的。 - Yakk - Adam Nevraumont

3

由于您将问题标记为C++14,因此不应使用函数指针!

在C++14中,您应该优先使用std::function和lambda表达式。

此外,您不应使用C风格数组,而只应使用std::array和/或std::vector

还要避免使用裸指针,使用std::unique_ptrstd::shared_ptr

因此,解决它的最简单和最好的方法是:

std::array<std::function<ParentClass*()>,5> arr {
    []() { return fun1(); },
    []() { return fun2(); },
    []() { return fun3(true); },
    []() { return fun4(); },
    []() { return fun5(7, 9); }
};

为什么不直接使用像@Quentin回答中那样的简单指针数组?他使用了lambda表达式,但是如果需要绑定任何内容,则无法使用lambda表达式。

2
因为std::function是一个重量级的工具,而且手头的问题不需要它提供的任何功能,只需要普通的函数指针,所以被踩了。 - Quentin

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