在C++中使用成员函数指针数组

4

在处理“命令和控制”类型的场景中,我经常大量使用函数指针,其中一个消息被发送到进程中,根据请求执行一个函数。这样实现起来非常高效,因为不再需要使用 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++中不仍然有效吗? - JeremyP
我不是现代C++的专家,但是既然它现在有了lambdas,你不能创建一个lambda的数组或向量或其他什么吗? - JeremyP
只是好奇,为什么你被switch-case卡住了?为什么不用跳转表呢? - mnistic
感谢您提出这个非常有趣的问题,并给予了赞!请查看我的答案,它对于任何模板方法、任意数量的类、任意数量和类型的参数(所有命令的不同参数数量)以及所有命令的任意不同结果类型都实现了解决方案。我的解决方案也非常高效,如果不考虑重度模板逻辑,那么实际最终编译后的运行时代码(和汇编代码)非常高效,它使用直接的switch表来调用具体的函数,没有其他多余的东西。 - Arty
3个回答

1
默认的解决方案确实是使用虚函数表:声明一个基类/接口,为每个消息定义一个虚方法。
您需要一个 switch(msg.cmd) 语句来调用相应的函数,但这基本上会替换掉您对函数表的初始化。
您将获得更清晰的处理,switch 语句甚至可以进行参数转换(因此消息处理程序会获得“有意义”的参数)。
您将失去一些表的“可组合性”,即将相同的处理程序函数“分配”给不相关的具体对象。
另一个更通用的选择是用std::function<void(MyMsg const &)>替换函数指针的元素,这不仅允许分配全局/静态函数,还可以分配任何其他类的成员函数、lambda等。您可以轻松地转发到现有函数,其签名不匹配。
缺点在于初始化表的成本更高,因为在一般情况下构造std::function可能涉及至少一个分配。此外,我至少会预计每次调用的成本更高,因为您将错过典型的虚表特定优化。
在这一点上,您可能还希望考虑不同的架构:至少对于每个消息有多个“侦听器”的情况,您可能希望考虑事件订阅或信号/插槽设计。
当然,你也可以坚持在C语言中的做法。这样也没问题。

0
嗯。这有点晚了,但我过去使用过以下其中之一:
  1. 如果你想要一堆相关的(从同一个基类派生)类对象,那么经典的面向对象方法就是隐式地为所有虚函数创建一个虚函数表。

  2. 然而有时候,你可能想要将一堆行为建模到一个类或命名空间中,以减少样板代码并获得额外的速度。这是因为:

    • 不同类的虚函数表可能存储在内存的任何位置,很可能不是连续的区域。这会减慢查找速度,因为你要查找的表项很可能不在缓存中。

    • 仅将你感兴趣的函数的虚函数表存储在数组中,会将指针局部化到内存中的连续位置,使其更有可能在缓存中。此外,如果你幸运的话,一些编译器优化器甚至可以将代码内联,因为它会像在 switch case 语句中一样。

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

    函数指针数组不仅限于常规函数,还可以使用成员函数指针。这有点棘手,但一旦你掌握了它,就不会太难。参见 “.*”和“->*”运算符之间的区别是什么? 以获取示例。直接使用函数指针是可行的,但建议使用 invoke() 函数,因为优先级规则需要额外的括号,这可能会变得混乱。

总结:

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

你所做的实际上取决于你的情况,你试图模拟什么,特别是你的要求。


0

非常感谢有趣的问题! 我很高兴能够从头开始实现自己的解决方案。

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

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

为了在单个函数中返回许多可能的类型,我使用了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)

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

代码的完整示例(答案末尾)展示了我MethodsRunner的所有用法类型。

我在我的解决方案中使用了switch结构,而不是std::vector<std::function<void()>>,因为只有switch才能处理任意参数类型和数量以及任意返回类型的一组数据。只有在所有命令具有相同的参数类型和返回值时,才可以使用std::function表代替switch

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

我的解决方案应该非常高效,尽管它似乎包含了很多代码,但所有重型模板化的代码都被折叠成非常小的实际运行时代码,基本上只有一个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

其中MR是专为所有命令定制的MethodsRunner类型。这里,单个对象运行器arbr都可调用,就像函数一样,签名为(cmd_id, args...),例如br(4, 12, true)调用意味着命令ID为4,参数为12, true,并且b对象本身是通过br(b);在构造时捕获到的SingleObjectRunner内部。

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

在线尝试!

#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 <cxxabi.h>来实现 TypeName<T>() 函数,此头文件仅用于名称重整的目的。此头文件在MSVC编译器中不可用,并且在Windows版本的CLang中可能不可用。在MSVC中,您可以删除#include <cxxabi.h>,并在 TypeName<T>() 中不进行重整,只需返回 return typeid(T).name();。这个头文件是我代码中唯一的非交叉编译的部分,如果需要,您可以轻松移除对该头文件的使用。


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