【问题标题】:Using an array of function pointers to member functions in C++在 C++ 中使用指向成员函数的函数指针数组
【发布时间】:2018-05-12 03:49:17
【问题描述】:

在处理“命令和控制”类型的场景时,我经常大量使用函数指针,其中消息被发送到根据请求执行函数的进程。这使得实现相当有效,因为不再需要使用switch-case(抛开跳表优化)来做这样的事情。例如:

而不是这个:

switch(msg.cmd){
    case FUNC0:
        return func0(msg);

    case FUNC1:
        return func1(msg);

    ...
}

我们可以这样做来直接执行适当的处​​理程序(省略对 msg.cmd 的任何健全性检查):

(*cmd_functions[msg.cmd])(msg)

最近,我开始使用实现类似“控制”功能的 C++ 代码,但我坚持使用 switch-case。是否有在 C++ 中执行此操作的规范方法?也许是在构造函数中初始化的函数指针数组实例变量?

我担心由于运行时使用类的 V 表,解决方案可能会稍微复杂一些。

【问题讨论】:

  • C 方法在 C++ 中还不能工作吗?
  • 我不是现代 C++ 方面的专家,但是,既然它现在有 lambdas,你能不创建一个数组或向量或任何 lambdas 吗?
  • 只是好奇,为什么你会被开关盒卡住?为什么跳表不起作用?
  • 感谢您提出非常有趣的问题,投票赞成!请查看my answer 的问题,它非常复杂,但为任何模板化方法、任意数量的类、任意数量的参数和类型(所有命令的参数数量不同)和任何不同的结果类型实现了解决方案对于所有命令。我的解决方案也非常高效,如果不看重模板逻辑,那么实际最终编译的运行时代码(和汇编程序)非常高效,它使用直接switch表来调用具体函数,没有别的跨度>

标签: c++ function-pointers member-function-pointers


【解决方案1】:

感谢您提出非常有趣的问题!我很高兴从头开始实施我自己的解决方案。

对于常规的非模板函数,没有返回类型和没有参数,将所有函数包装到std::vector<std::function<void()>> cmds 并通过索引调用必要的命令是微不足道的。

但我决定实现一个相当复杂的解决方案,以便能够处理具有任何返回类型的任何模板化函数(方法),因为模板在 C++ 世界中很常见。我的答案末尾的完整代码是完整且有效的,main() 函数中提供了许多示例。可以复制粘贴代码以在您自己的项目中使用。

为了在单个函数中返回许多可能的类型,我使用了std::variant,这是 C++17 的标准类。

使用我的完整代码(在答案的底部),您可以执行以下操作:

using MR = MethodsRunner<
    Cmd<1, &A::Add3>,
    Cmd<2, &A::AddXY<long long>>,
    Cmd<3, &B::ToStr<std::integral_constant<int, 17>>>,
    Cmd<4, &B::VoidFunc>
>;

A a;
B b;
auto result1 = MR::Run(1, a, 5);        // Call cmd 1
auto result2 = MR::Run(2, a, 3, 7);     // Call cmd 2
auto result3 = MR::Run(3, b);           // Call cmd 3
auto result4 = MR::Run(4, b, 12, true); // Call cmd 4
auto result4_bad = MR::Run(4, b, false); // Call cmd 4, wrong arguments, exception!
auto result5 = MR::Run(5, b);           // Call to unknown cmd 5, exception!

这里的AB 是任何类。对于MethodsRunner,您提供了一个命令列表,其中包含命令 ID 和指向方法的指针。只要您提供他们调用的完整签名,您就可以提供指向任何模板化方法的指针。

MethodsRunner 在被.Run() 调用时返回std::variant,其中包含所有不同类型的可能值。您可以通过std::get(variant) 访问变量的实际值,或者如果您事先不知道包含的类型,您可以使用std::visit(lambda, variant)

我的 MethodsRunner 的所有类型的用法都显示在完整代码的 main() 中(在答案的末尾)。

在我的课堂上,我使用了几个小型辅助模板结构,这种元编程在模板化 C++ 世界中非常常见。

我在我的解决方案中使用switch 构造而不是std::vector&lt;std::function&lt;void()&gt;&gt;,因为只有switch 将允许处理一组任意参数类型、计数和任意返回类型。 std::function table 可以代替 switch 仅在所有命令具有相同的参数类型和相同的返回值的情况下。

众所周知,如果 switch 和 case 值是整数,所有现代编译器都将switch 实现为直接跳转表。换句话说,switch 解决方案与常规 std::vector&lt;std::function&lt;void()&gt;&gt; 函数表方法一样快,甚至可能更快。

我的解决方案应该是非常高效的,虽然看起来包含很多代码,但我的所有繁重的模板化代码都被折叠成非常小的实际运行时代码,基本上有一个switch 表直接调用所有方法,加上转换为 std::variant 以获取返回值,就是这样,几乎没有任何开销。

我希望您使用的命令 id 在编译时是未知的,我希望它仅在运行时才知道。如果在编译时就知道了,那么就不需要switch,基本上可以直接调用给定的对象。

我的 Run 方法的语法是 method_runner.Run(cmd_id, object, arguments...),在这里你提供任何只有在运行时才知道的命令 id,然后你提供任何对象和任何参数。如果您只有一个实现所有命令的对象,那么您可以使用我在代码中也实现的SingleObjectRunner,如下所示:

SingleObjectRunner<MR, A> ar(a);
ar(1, 5);         // Call cmd 1
ar(2, 3, 7);      // Call cmd 2

SingleObjectRunner<MR, B> br(b);
br(3);            // Call cmd 3
br(4, 12, true);  // Call cmd 4

其中MRMethodsRunner 的类型,专门用于所有命令。这里单个对象运行器arbr 都是可调用的,就像函数一样,带有签名(cmd_id, args...),例如br(4, 12, true) 调用意味着cmd id 是4,args 是12, true,和b 对象本身在构造时通过br(b);在SingleObjectRunner内部捕获

查看代码后的详细控制台输出日志。另请参阅代码和日志后的重要注意。完整代码如下:

Try it online!

#include <iostream>
#include <type_traits>
#include <string>
#include <any>
#include <vector>
#include <tuple>
#include <variant>
#include <iomanip>
#include <stdexcept>

#include <cxxabi.h>

template <typename T>
inline std::string TypeName() {
    // use following line of code if <cxxabi.h> unavailable, and/or no demangling is needed
    //return typeid(T).name();
    int status = 0;
    return abi::__cxa_demangle(typeid(T).name(), 0, 0, &status);
}

struct NotCallable {};
struct VoidT {};

template <size_t _Id, auto MethPtr>
struct Cmd {
    static size_t constexpr Id = _Id;

    template <class Obj, typename Enable = void, typename ... Args>
    struct Callable : std::false_type {};

    template <class Obj, typename ... Args>
    struct Callable<Obj,
        std::void_t<decltype(
            (std::declval<Obj>().*MethPtr)(std::declval<Args>()...)
        )>, Args...> : std::true_type {};

    template <class Obj, typename ... Args>
    static auto Call(Obj && obj, Args && ... args) {
        if constexpr(Callable<Obj, void, Args...>::value) {
            if constexpr(std::is_same_v<void, std::decay_t<decltype(
                (obj.*MethPtr)(std::forward<Args>(args)...))>>) {
                (obj.*MethPtr)(std::forward<Args>(args)...);
                return VoidT{};
            } else
                return (obj.*MethPtr)(std::forward<Args>(args)...);
        } else {
            throw std::runtime_error("Calling method '" + TypeName<decltype(MethPtr)>() +
                "' with wrong object type and/or wrong argument types or count and/or wrong template arguments! "
                "Object type '" + TypeName<Obj>() + "', tuple of arguments types '" + TypeName<std::tuple<Args...>>() + "'.");
            return NotCallable{};
        }
    }
};

template <typename T, typename ... Ts>
struct HasType;

template <typename T>
struct HasType<T> : std::false_type {};

template <typename T, typename X, typename ... Tail>
struct HasType<T, X, Tail...>  {
    static bool constexpr value = std::is_same_v<T, X> ||
        HasType<T, Tail...>::value;
};

template <typename T> struct ConvVoid {
    using type = T;
};

template <> struct ConvVoid<void> {
    using type = VoidT;
};

template <typename V, typename ... Ts>
struct MakeVariant;

template <typename ... Vs>
struct MakeVariant<std::variant<Vs...>> {
    using type = std::variant<Vs...>;
};

template <typename ... Vs, typename T, typename ... Tail>
struct MakeVariant<std::variant<Vs...>, T, Tail...> {
    using type = std::conditional_t<
        HasType<T, Vs...>::value,
        typename MakeVariant<std::variant<Vs...>, Tail...>::type,
        typename MakeVariant<std::variant<Vs...,
            typename ConvVoid<std::decay_t<T>>::type>, Tail...>::type
    >;
};

template <typename ... Cmds>
class MethodsRunner {
public:
    using CmdsTup = std::tuple<Cmds...>;
    static size_t constexpr NumCmds = std::tuple_size_v<CmdsTup>;
    template <size_t I> using CmdAt = std::tuple_element_t<I, CmdsTup>;

    template <size_t Id, size_t Idx = 0>
    static size_t constexpr CmdIdToIdx() {
        if constexpr(Idx < NumCmds) {
            if constexpr(CmdAt<Idx>::Id == Id)
                return Idx;
            else
                return CmdIdToIdx<Id, Idx + 1>();
        } else
            return NumCmds;
    }

    template <typename Obj, typename ... Args>
    using RetType = typename MakeVariant<std::variant<>, decltype(
        Cmds::Call(std::declval<Obj>(), std::declval<Args>()...))...>::type;

    template <typename Obj, typename ... Args>
    static RetType<Obj, Args...> Run(size_t cmd, Obj && obj, Args && ... args) {
        #define C(Id) \
            case Id: { \
                if constexpr(CmdIdToIdx<Id>() < NumCmds) \
                    return CmdAt<CmdIdToIdx<Id>()>::Call( \
                        obj, std::forward<Args>(args)... \
                    ); \
                else goto out_of_range; \
            }

        switch (cmd) {
            C(  0) C(  1) C(  2) C(  3) C(  4) C(  5) C(  6) C(  7) C(  8) C(  9)
            C( 10) C( 11) C( 12) C( 13) C( 14) C( 15) C( 16) C( 17) C( 18) C( 19)
            default:
                goto out_of_range;
        }

        #undef C

        out_of_range:
        throw std::runtime_error("Unknown command " + std::to_string(cmd) +
            "! Number of commands " + std::to_string(NumCmds));
    }
};

template <typename MR, class Obj>
class SingleObjectRunner {
public:
    SingleObjectRunner(Obj & obj) : obj_(obj) {}
    template <typename ... Args>
    auto operator () (size_t cmd, Args && ... args) {
        return MR::Run(cmd, obj_, std::forward<Args>(args)...);
    }
private:
    Obj & obj_;
};

class A {
public:
    int Add3(int x) const {
        std::cout << "Add3(" << x << ")" << std::endl;
        return x + 3;
    }
    template <typename T>
    auto AddXY(int x, T y) {
        std::cout << "AddXY(" << x << ", " << y << ")" << std::endl;
        return x + y;
    }
};

class B {
public:
    template <typename V>
    std::string ToStr() {
        std::cout << "ToStr(" << V{}() << ")" << std::endl;
        return "B_ToStr " + std::to_string(V{}());
    }
    void VoidFunc(int x, bool a) {
        std::cout << "VoidFunc(" << x << ", " << std::boolalpha << a << ")" << std::endl;
    }
};

#define SHOW_EX(code) \
    try { code } catch (std::exception const & ex) { \
        std::cout << "\nException: " << ex.what() << std::endl; }

int main() {
    try {
        using MR = MethodsRunner<
            Cmd<1, &A::Add3>,
            Cmd<2, &A::AddXY<long long>>,
            Cmd<3, &B::ToStr<std::integral_constant<int, 17>>>,
            Cmd<4, &B::VoidFunc>
        >;

        auto VarInfo = [](auto const & var) {
            std::cout
                << ", var_idx: " << var.index()
                << ", var_type: " << std::visit([](auto const & x){
                    return TypeName<decltype(x)>();
                }, var)
                << ", var: " << TypeName<decltype(var)>()
                << std::endl;
        };

        A a;
        B b;

        {
            auto var = MR::Run(1, a, 5);

            std::cout << "cmd 1: var_val: " << std::get<int>(var);
            VarInfo(var);
        }

        {
            auto var = MR::Run(2, a, 3, 7);

            std::cout << "cmd 2: var_val: " << std::get<long long>(var);
            VarInfo(var);
        }

        {
            auto var = MR::Run(3, b);

            std::cout << "cmd 3: var_val: " << std::get<std::string>(var);
            VarInfo(var);
        }

        {
            auto var = MR::Run(4, b, 12, true);

            std::cout << "cmd 4: var_val: VoidT";
            std::get<VoidT>(var);
            VarInfo(var);
        }

        std::cout << "------ Single object runs: ------" << std::endl;

        SingleObjectRunner<MR, A> ar(a);
        ar(1, 5);
        ar(2, 3, 7);

        SingleObjectRunner<MR, B> br(b);
        br(3);
        br(4, 12, true);

        std::cout << "------ Runs with exceptions: ------" << std::endl;

        SHOW_EX({
            // Exception, wrong argument types
            auto var = MR::Run(4, b, false);
        });

        SHOW_EX({
            // Exception, unknown command
            auto var = MR::Run(5, b);
        });

        return 0;
    } catch (std::exception const & ex) {
        std::cout << "Exception: " << ex.what() << std::endl;
        return -1;        
    }
}

输出:

Add3(5)
cmd 1: var_val: 8, var_idx: 0, var_type: int, var: std::variant<int, NotCallable>
AddXY(3, 7)
cmd 2: var_val: 10, var_idx: 1, var_type: long long, var: std::variant<NotCallable, long long>
ToStr(17)
cmd 3: var_val: B_ToStr 17, var_idx: 1, var_type: std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, var: std::variant<NotCallable, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >
VoidFunc(12, true)
cmd 4: var_val: VoidT, var_idx: 1, var_type: VoidT, var: std::variant<NotCallable, VoidT>
------ Single object runs: ------
Add3(5)
AddXY(3, 7)
ToStr(17)
VoidFunc(12, true)
------ Runs with exceptions: ------

Exception: Calling method 'void (B::*)(int, bool)' with wrong object type and/or wrong argument types or count and/or wrong template arguments! Object type 'B', tuple of arguments types 'std::tuple<bool>'.

Exception: Unknown command 5! Number of commands 4

注意:在我的代码中,我使用了#include &lt;cxxabi.h&gt; 来实现TypeName&lt;T&gt;() 函数,这个包含的标头仅用于名称分解目的。此标头在 MSVC 编译器中不可用,并且可能在 Windows 版本的 CLang 中不可用。在 MSVC 中,您可以删除 #include &lt;cxxabi.h&gt;,而在 TypeName&lt;T&gt;() 内部,只需返回 return typeid(T).name(); 就不会进行拆解。此标头是我的代码中唯一不可交叉编译的部分,如果需要,您可以轻松删除此标头的使用。

【讨论】:

    【解决方案2】:

    嗯。这有点晚了,但我过去使用过以下之一:

    1. 经典的 OO 方法是,如果您想要的是一堆相关的(从同一个基类派生的)类对象,它们隐式地为所有虚函数创建一个 vtable。

    2. 但有时,您可能希望对包含在一个类或命名空间中的一堆行为进行建模,以减少样板文件并获得额外的速度。这是因为:

      • 不同类的 vtable 可以存储在内存中的任何位置,而且很可能不在连续区域中。这会减慢查找速度,因为您要查找的表条目很可能不在缓存中。

      • 在数组中仅保留您感兴趣的函数的 vtable 会将指针本地化到内存中的连续位置,使其更有可能位于缓存中。此外,如果幸运的话,一些编译器的优化器可能已经足够先进,甚至可以像在 switch case 语句中那样内联代码。

      使用函数指针数组,您可以存储指向全局函数(最好封装在命名空间中)或静态成员函数(最好在类的私有部分中)的常规函数​​指针,这样它们就不会直接可在课堂外访问)。

      函数指针数组不限于普通函数,还可以使用成员函数指针。这些使用起来有点棘手,但一旦你掌握了它并不会太糟糕。有关示例,请参阅What is the difference between the .* and ->* operators?。直接使用函数指针是可行的,但正如那里所建议的那样,最好使用 invoke() 函数,因为它可能会因需要额外括号的优先规则而变得混乱。

    夏天:

    • 所有这些方法都可以将函数保留在类或命名空间中,而运行时开销最小甚至没有,尽管 OO 方式确实包含更多的编码开销。
    • 具有虚函数或成员函数指针的类都允许访问类数据,这可能是有益的。
    • 如果速度至关重要,(成员)函数指针数组可能会为您提供所需的额外提升。

    您所做的实际上取决于您的情况、您尝试建模的内容,尤其是您的要求。

    【讨论】:

      【解决方案3】:

      默认的解决方案确实是一个 v-table:为每条消息声明一个带有虚拟方法的基类/接口。

      您需要一个 switch(msg.cmd) - 语句来调用相应的函数 - 但这基本上会取代您对函数表的初始化。

      您会得到更简洁的处理,switch 语句甚至可以进行参数转换(因此消息处理程序得到“有意义的”参数)。

      您将失去表的一些“可组合性”,即将相同的处理函数“分配”给不同的、不相关的具体对象。


      另一个更通用的选择是将函数指针的元素替换为std::function&lt;void(MyMsg const &amp;)&gt; - 这不仅允许分配全局/静态函数,还允许分配任何其他类的成员函数、lambda 等。您可以轻松转发到签名不匹配的现有函数。

      这里的缺点是初始化表的成本更高,至少在一般情况下,构造 std::function 可能会涉及分配。此外,我预计至少目前每次调用的成本会更高,因为您会错过典型的 v-table 特定优化。


      此时,您可能还需要考虑不同的架构:至少对于每个消息有多个“侦听器”的情况,您可能需要考虑事件订阅或信号/插槽设计。


      当然,你也可以坚持你在 C 中所做的方式。没关系。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-04-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多