简体   繁体   中英

Using an array of function pointers to member functions in C++

I have often made liberal use of function pointers when working with 'command and control'-type scenarios where a message is sent to a process wherein a function is executed based on the request. This makes for a reasonably efficient implementation, as one no longer needs to do such a thing using a switch-case (jump table optimizations aside). For example:

Instead of this:

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

    case FUNC1:
        return func1(msg);

    ...
}

We could do something like this to execute the appropriate handler directly (omitting any sanity checking on msg.cmd ):

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

Recently, I have started working with C++ code that implements similar "control" functionality, but I am stuck doing so using a switch-case. Is there a canonical methodology for doing this in C++? Perhaps a function-pointer array instance variable initialized in the constructor?

I was concerned the solution might be a little more complicated due to runtime use of a class's V-table.

The default solution would indeed be a v-table: declare a base class/interface with a virtual method for each message.

You'd need a switch(msg.cmd) - statement to call the respective function - but that would basically replace your initialization of the function table.

You'd get cleaner handling, the switch statement could even do parameter translation (so the message handlers get "meaningful" arguments).

You would lose some "composeability" of the table, ie "assigning" the same handler function to different, unrelated concrete objects.


Another, more generic option would be replacing the elements of the function pointers with std::function<void(MyMsg const &)> - this would allow not only assigning global/static functions, but also any other class' member functions, lambdas and the like. You could easily forward to existing functions whose signatures do not match.

The downside here is higher cost of initializing the table, sicne constructing the std::function will probably involve an allocaiton at least in the general case. Also, I would expect higher cost per call at least for the time being, since you'd be missing out on typical v-table-specific optimizations.


At this point, you might also want to consider a different architecture: at least for the case where there are multiple "listeners" to each message, you might want to consider an event subscription or signal/slot design.


Of course, you can also stick to the way you did in C. It's fine.

Thanks for very interesting Question! I was happy to implement from scratch my own solution for it.

With regular non-templated functions, without return type and without arguments, it is trivial to wrap all functions into std::vector<std::function<void()>> cmds and call necessary command by index.

But I decided to implement a considerably more complex solution, so that is is able to handle any templated function (method) with any return type, because templates are common in C++ world. Full code at the end of my answer is complete and working, with many examples provided in main() function. Code can be copy-pasted to be used in your own projects.

To return many possible types in single function I used std::variant , this is C++17's standard class.

With my full code (at the bottom of answer) you can do following:

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!

Here A and B are any classes. To MethodsRunner you provide a list of commands, consisting of command id and pointer to method. You can provide pointers to any templated method as far as you provide full signature of theirs call.

MethodsRunner when called by .Run() returns std::variant containing all possible values with different types. You can access actual value of a variant through std::get(variant) , or if you don't know contained type in advance you can use std::visit(lambda, variant) .

All types of usages of my MethodsRunner are shown in the main() of full code (at the end of answer).

In my class I use several tiny helper templated structs, this kind of meta-programming is very common in templated C++ world.

I used switch construct in my solution instead of std::vector<std::function<void()>> , because only switch will allow to handle a pack of arbitrary argument types and count and arbitrary return type. std::function table can be used instead of switch only in the case if all commands have same arguments types and same return value.

It is well known that all modern compilers implement switch as direct jump table if switch and case values are integers. In other words switch solution is as fast, and maybe even faster than regular std::vector<std::function<void()>> function-table approach.

My solution should be very efficient, although it seems to contain a lot of code, all the heavy-templated my code is collapsed into very tiny actual runtime code, basically there is a switch table that directly calls all methods, plus conversion to std::variant for return value, that's it, almost no overhead at all.

I expected that your command id that you use is not known at compile time, I expected that it is known only at runtime. If it is known at compile time then there is no need for switch at all, basically you can directly call given object.

Syntax of my Run method is method_runner.Run(cmd_id, object, arguments...) , here you provide any command id known only at run time, then you provide any object and any arguments. If you have only single object that implements all the commands then you can use SingleObjectRunner that I implemented too in my code, like following:

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

where MR is type of MethodsRunner specialized for all commands. Here single objects runners ar and br are both callable, like functions, with signature (cmd_id, args...) , for example br(4, 12, true) call means cmd id is 4 , args are 12, true , and b object itself was captured inside SingleObjectRunner at construction time through br(b);

See detailed console output log after the code. See also important Note after the code and log. Full code below:

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;        
    }
}

Output:

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

Note : in my code I used #include <cxxabi.h> to implement TypeName<T>() function, this included header is used only for name-demangling purposes. This header is not available in MSVC compiler, and may not be available in Windows's version of CLang. In MSVC you can remove #include <cxxabi.h> and inside TypeName<T>() do no demangling by just returning return typeid(T).name(); . This header is the only non-cross-compilable part of my code, you can easily remove usage of this header if needed.

Hmmm. This is kinda late, but I've used in the past one of the following:

  1. The classic OO method would be if what you want is a bunch of related (derived from the same base) class objects, which implicitly creates a vtable for all virtual functions.

  2. Sometimes though, you may want to model a bunch of behaviours contained to one class or namespace to reduce boilerplate and to get some extra speed. This would be because:

    • Different class's vtables could be stored anywhere in memory, and more than likely not in a contiguous area. This would slow down lookup as the table entry you're looking for is most likely not in the cache.

    • Keeping a vtable of only the functions that you are interested in in an array, would be localised the pointers to a contiguous location in memory, making it far more likely to be in the cache. Also, if you're lucky, some compiler's optimizers may be advanced enough to even inline the code as it would be in a switch case statement.

    With a function pointer array, you could store regular function pointers that point to either global functions (preferably encapsulated in a namespace) or as static member functions (preferably in a private part of the class so that they wouldn't be directly accessible outside of the class).

    Function pointer arrays are not limited to regular functions, but could use member function pointers. These are a little tricky to use, but are not too bad once you get the hang of it. See What is the difference between the .* and ->* operators? for an example. Using the function pointer directly is doable, but as suggested there, it's preferable to use the invoke() function as it can get messy with precedence rules requiring extra parentheses.

In summery:

  • All of these methods can keep the functions inside of the class or namespace with minimal to no runtime overhead, though the OO way does contain more coding overhead.
  • Classes with virtual function or member function pointers both allow access to class data, which could be beneficial.
  • If speed is of the essence, a (member) function pointer array may give you that extra boost you need.

What you do really depends on your circumstances, what you're trying to model and especially your requirements.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM