簡體   English   中英

在 C++ 中使用指向成員函數的函數指針數組

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

在處理“命令和控制”類型的場景時,我經常自由地使用函數指針,在這些場景中,消息被發送到一個進程,其中一個函數是根據請求執行的。 這使得實現相當有效,因為不再需要使用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 表,解決方案可能會更復雜一些。

默認解決方案確實是一個v表:使用每個消息的虛方法聲明一個基類/接口。

你需要一個switch(msg.cmd) - 語句來調用相應的函數 - 但這基本上會取代你對函數表的初始化。

你會得到更清晰的處理,switch語句甚至可以進行參數轉換(所以消息處理程序得到“有意義的”參數)。

您將失去該表的某些“組合性”,即將相同的處理函數“分配”給不同的,不相關的具體對象。


另一個更通用的選項是用std::function<void(MyMsg const &)>替換函數指針的元素 - 這不僅可以分配全局/靜態函數,還可以分配任何其他類的成員函數,lambda和類似。 您可以輕松轉發到簽名不匹配的現有函數。

這里的缺點是初始化表的成本較高,構造std :: function的sicne可能至少在一般情況下涉及一個分配。 此外,我預計至少目前會有更高的每次通話費用,因為你會錯過典型的特定於v表的優化。


此時,您可能還需要考慮不同的體系結構:至少對於每條消息有多個“偵聽器”的情況,您可能需要考慮事件訂閱或信號/插槽設計。


當然,你也可以堅持你在C中的方式。這很好。

感謝非常有趣的問題! 我很高興從頭開始實施我自己的解決方案。

對於常規的非模板化函數,沒有返回類型和參數,將所有函數包裝到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<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)調用意味着 cmd id 是4 , args 是12, trueb對象本身在構建時通過br(b);被捕獲在 SingleObjectRunner 中br(b);

代碼后查看詳細的控制台輸出日志。 另請參閱代碼和日志后的重要注釋 完整代碼如下:

在線試試吧!

#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(); . 此標頭是我的代碼中唯一不可交叉編譯的部分,如果需要,您可以輕松刪除此標頭的使用。

嗯。 這有點晚了,但我過去曾使用過以下方法之一:

  1. 經典的 OO 方法是,如果您想要的是一堆相關的(派生自同一個基類)類對象,它會為所有虛函數隱式地創建一個 vtable。

  2. 但有時,您可能希望對包含在一個類或命名空間中的一系列行為進行建模,以減少樣板並獲得一些額外的速度。 這將是因為:

    • 不同類的 vtable 可以存儲在內存中的任何位置,而且很可能不在連續區域中。 這會減慢查找速度,因為您要查找的表條目很可能不在緩存中。

    • 在數組中僅保留您感興趣的函數的 vtable,會將指針本地化到內存中的連續位置,使其更有可能在緩存中。 此外,如果幸運的話,一些編譯器的優化器可能足夠先進,甚至可以像在 switch case 語句中那樣內聯代碼。

    使用函數指針數組,您可以存儲指向全局函數(最好封裝在命名空間中)或靜態成員函數(最好在類的私有部分,以便它們不能在外部直接訪問)的常規函數​​指針班上)。

    函數指針數組不限於常規函數,還可以使用成員函數指針。 這些使用起來有點棘手,但一旦掌握了它,就不會太糟糕。 請參閱.* 和 ->* 運算符之間的區別是什么? 舉個例子。 直接使用函數指針是可行的,但正如那里所建議的,最好使用invoke()函數,因為它可能會因需要額外括號的優先規則而變得混亂。

在夏天:

  • 盡管 OO 方式確實包含更多的編碼開銷,但所有這些方法都可以將函數保持在類或命名空間內,運行時開銷最小甚至沒有。
  • 具有虛函數或成員函數指針的類都允許訪問類數據,這可能是有益的。
  • 如果速度至關重要,則(成員)函數指針數組可能會為您提供所需的額外提升。

您所做的實際上取決於您的情況、您嘗試建模的內容,尤其是您的要求。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM