V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
aqtata
V2EX  ›  C++

这种情况如何消除几百个 if/else

  •  2
     
  •   aqtata · 2 天前 · 7114 次点击

    运行时从外部读取一个 16 进制数字,然后调用对应的函数,比如读取到1F3,那么就调用函数foo_1f3,函数参数也是有编号的,规律是这样:

    void foo_1f0(myclass_1f0& val);
    void foo_1f1(myclass_1f1& val);
    void foo_1f2(myclass_1f2& val);
    

    之前 C#是用的反射,很容易实现。到 C++这不知道怎么搞比较优雅,目前有上百个 if/else 去判断然后调用。

    C++这边可以用到 C++20 ,不知道有什么酷的解决方法?

    63 条回复    2025-01-18 06:30:40 +08:00
    lichao
        1
    lichao  
       2 天前   ❤️ 1
    std::map
    liuguangxuan
        2
    liuguangxuan  
       2 天前   ❤️ 1
    模板。
    blu10ph
        3
    blu10ph  
       2 天前   ❤️ 2
    你是否在找:工厂模式/策略模式?

    感觉你描述的还比较清晰,把你发的这段内容交给 AI,一会就能出来代码~
    yuzii
        4
    yuzii  
       2 天前   ❤️ 1
    std::vector<std::function<void(int)>> foos;
    aqtata
        5
    aqtata  
    OP
       2 天前
    @yuzii 参数不同,而且注册也得写几百行,感觉和 if/else 差不多。
    aqtata
        6
    aqtata  
    OP
       2 天前
    @lichao 注册函数?感觉和 if/else 差不多,也要写上百行。
    aqtata
        7
    aqtata  
    OP
       2 天前
    @liuguangxuan 展开讲讲。。
    YakumoZi
        8
    YakumoZi  
       2 天前   ❤️ 1
    @liuguangxuan 模板是编译时计算,他这个要求运行时读取,应该做不到吧?
    zwy100e72
        9
    zwy100e72  
       2 天前   ❤️ 1
    对于分发用的标志 `1fx` 比较集中的情况,可以直接用 `std::vector<std::function<>>` 然后用数组下标做 key ;对于 key 比较稀疏的情况,可以用 `std::map<int, std::function<>>` 或者 `std::unordered_map`

    注册要写几百行这种估计跑不掉的,目前 c++ 还没有正式支持反射;不过可以用部分编译器支持的 `[[constructor]]` 语法,在启动阶段实现自动注册。例如有某 `Dispatcher::register(int, std::function<>)` 方法,部分编译器可以实现:

    ```c++
    [[gnu::constructor]] reg_handler() { Dispatcher::register(0x1f3, foo_1f3); }
    ```
    fyxtc
        10
    fyxtc  
       2 天前   ❤️ 7
    对于无反射的静态编译型语言,这种情况 if 和 switch 是最清晰的,因为阅读代码的人只要看三个 if 就知道这坨是干什么的,也非常容易定位和修改,用其他自以为“优雅”的解决方案很大概率只是满足了自己苦了他人。
    Metatron7
        11
    Metatron7  
       2 天前   ❤️ 1
    不想写 ifelse 就 LLM 生成得了
    zwy100e72
        12
    zwy100e72  
       2 天前   ❤️ 1
    写了一个超省略的 demo ,供参考 https://godbolt.org/z/ox7K6sM1e
    leonshaw
        13
    leonshaw  
       2 天前 via Android   ❤️ 2
    直接 switch ,case 用宏包一下,再用工具生成。
    强行搞可以把函数导出,运行时查符号。
    guanzhangzhang
        14
    guanzhangzhang  
       2 天前
    怎么感觉你是在搞啥协议解析,前面是 msgID 对应不同的解析函数,去处理后面的 data 部分
    geelaw
        15
    geelaw  
       2 天前   ❤️ 2
    wow 这个问题完全 under-specified 。第一个问题:你知道要调用什么函数了,可你怎么制造不同类型的参数传入之?

    但楼主不应该尝试回答我的第一个问题,而是应该直接说自己实际上要解决的问题,而不是来问自己觉得可行的一半解决方法的另一半。
    chashao
        16
    chashao  
       1 天前   ❤️ 1
    额,感觉还得用脚本批量生成函数注册的逻辑。。
    ```c++
    void foo_1f0(int& value) { std::cout << "test!!" << std::endl; }

    // 这块代码得脚本生成
    std::unordered_map<std::string, std::function<void(int&)>> GFunctions = {
    { "foo_1f0", foo_1f0 }
    };

    int main()
    {
    int inParam = 0x1f0;

    std::stringstream ss;
    ss << std::hex<<inParam;
    std::string funcName = "foo_" + ss.str();

    int param = 1;
    GFunctions[funcName](param);
    }
    ```
    c3de3f21
        17
    c3de3f21  
       1 天前
    这样写是最好的
    sapphire
        18
    sapphire  
       1 天前   ❤️ 1
    这种情况适宜从整体项目 build 的角度考量,有工程上的设计和考量。如果语言本身没有反射机制,一般用脚本或其他自定义工具,在 Pre-build 阶段生成函数的桩,同时对参数和调用场景做必要的安全检查。这样对于函数实现代码,只要做好必要的标注,就和 C#没什么区别了。
    MoYi123
        19
    MoYi123  
       1 天前   ❤️ 1
    想要酷可以参考 std::visit 的做法. 编译期生成一个 Invoke_array, 下面的例子是从运行期的 int 转特定类型到 lambda 中的例子. 稍微改改就能用于你的需求.

    using namespace std;

    constexpr std::array cached_type_ints = {1};

    struct void_ptr {
    int type;
    void *ptr;
    };

    template <int T> struct Int2Type;

    template <> struct Int2Type<1> {
    using type = int;
    };

    template <typename Func, typename> class Visitor;

    template <typename Func, std::size_t... pos>
    class Visitor<Func, std::index_sequence<pos...>> {
    public:
    using return_type =
    std::invoke_result_t<Func, Int2Type<cached_type_ints[0]>::type *>;
    using fn_type = return_type (*)(Func &&, void *);

    template <int16_t type_int>
    static auto func_wrapper(Func &&func, void *ptr) -> return_type {
    using real_type = typename Int2Type<type_int>::type;
    return func(static_cast<real_type *>(ptr));
    }

    static auto visit(Func &&func, const void_ptr &item) -> return_type {
    constexpr static std::array<fn_type, cached_type_ints.size()> invoke_map = {
    func_wrapper<cached_type_ints[pos]>...};

    size_t idx = std::ranges::lower_bound(cached_type_ints.begin(),
    cached_type_ints.end(), item.type) -
    cached_type_ints.begin();
    if (idx >= invoke_map.size() or cached_type_ints[idx] != item.type)
    [[unlikely]] {
    throw std::bad_variant_access();
    }
    return invoke_map[idx](std::forward<Func>(func), item.ptr);
    }
    };

    template <typename Func> auto visit(Func &&func, const void_ptr &item) {
    using visitor = Visitor<decltype(func),
    std::make_index_sequence<cached_type_ints.size()>>;
    return visitor::visit(std::forward<Func>(func), item);
    }

    inline auto usage() {
    auto item = void_ptr{.ptr = new int(1), .type = 1};
    visit(
    [](auto *ptr) {
    print(*ptr);
    delete ptr;
    },
    item);
    }
    securityCoding
        20
    securityCoding  
       1 天前   ❤️ 1
    查表 map<key,func>
    HtPM
        21
    HtPM  
       1 天前   ❤️ 1
    宏定义
    qq135449773
        22
    qq135449773  
       1 天前   ❤️ 1
    这种需求本来难以简化的,Map<key, func>已经是不错的选择了。

    再想继续想办法简化我觉得只能增加后期维护负担。
    nuk
        23
    nuk  
       1 天前   ❤️ 1
    符号得和得和字符串对上,简单点就是 dlsym ,复杂一点用 linker set ,自己加 map 也可以,就是没那么优雅了。
    DLOG
        24
    DLOG  
       1 天前   ❤️ 1
    请问 您是找 “宏” 的使用方法么
    oneisall8955
        25
    oneisall8955  
       1 天前   ❤️ 1
    反射有没有?
    shadowyue
        26
    shadowyue  
       1 天前   ❤️ 1
    只是平级的 if else 没必要专门消除吧,看起来也是很清晰的。
    你要 if else 嵌套很多层,去消除更有意义一点。
    defaw
        27
    defaw  
       1 天前
    用 map ,key 是数字,value 是函数指针,直接写死在 map 的初始化里
    gam2046
        28
    gam2046  
       1 天前
    @DLOG #24 大佬,求明示,宏如何解决楼主的问题。能给个简单的样例嘛。
    xing7673
        29
    xing7673  
       1 天前
    @leonshaw 用宏包最满足要求
    levelworm
        31
    levelworm  
       1 天前 via Android
    函数指针数组就行了。直接写死。然后直接调用数组成员。
    sampeng
        32
    sampeng  
       1 天前
    查表法简单优雅。比冲击波墙吧。。。
    seanwhy
        33
    seanwhy  
       1 天前 via iPhone   ❤️ 1
    c++有个 rttr 的反射开源库,github 上可以搜下
    exonuclease
        34
    exonuclease  
       1 天前
    代码生成?
    jcharr
        35
    jcharr  
       1 天前
    ```c
    jcharr
        36
    jcharr  
       1 天前
    ```c
    #define GET_HEX_REGITER(HEX) \
    static __always_inline void ctx_##HEX(myclass_1##HEX& val) { \
    /*todo*/ \
    }

    GET_HEX_REGITER(a)
    ```
    这是用宏来写的 感觉应该能满足吧 能不用映射就不用映射
    min
        37
    min  
       1 天前
    这个问题你找不到大模型可以问吗?
    netabare
        38
    netabare  
       1 天前 via iPhone
    用表驱动和高阶函数会比较好
    nicebird
        39
    nicebird  
       1 天前
    宏定义吧
    nicebird
        40
    nicebird  
       1 天前
    另外现在直接用 copilot 生成就行了,也不用写啥代码
    minami
        41
    minami  
       1 天前
    正解是 libffi
    chandlerbing9317
        42
    chandlerbing9317  
       1 天前
    分支比较多改成表驱动就好了
    ETiV
        43
    ETiV  
       1 天前 via iPhone
    都是 16 进制范围内就用偏移做呗
    LuJyKa
        44
    LuJyKa  
       1 天前
    把这堆函数导出成 Dll ,然后用 GetProcAddress 直接传字符串得到函数地址,然后把参数地址传进去。
    vituralfuture
        45
    vituralfuture  
       1 天前 via Android
    写个脚本,在构建阶段生成文件参与编译
    liuidetmks
        46
    liuidetmks  
       1 天前 via iPhone
    @jcharr 要 runtime 吧
    FrankFang128
        47
    FrankFang128  
       1 天前
    选中代码,让 AI 消除 if else
    198plus
        48
    198plus  
       1 天前
    建议代码生成,用“写代码的代码”维护代码,我记得 rust 编译器里面就有这种东西,用 python 脚本生成 rust 代码
    bluearc
        49
    bluearc  
       1 天前   ❤️ 1
    用模板就可以解决,

    ```
    #include <iostream>
    #include <string>
    #include <sstream>
    #include <stdexcept>
    #include <cstdint>


    template<std::uint32_t N>
    struct myclass {
    void print() const {
    throw std::logic_error("Error: myclass<" + std::to_string(N) + "> is not specialized.");
    }
    };

    // 特化模板(如果需要为某些编号提供特定行为)
    template<>
    struct myclass<0x1f0> {
    void print() const { std::cout << "Specialized myclass<1f0>\n"; }
    };

    template<>
    struct myclass<0x2f0> {
    void print() const { std::cout << "Specialized myclass<2f0>\n"; }
    };

    // 通用模板函数
    template<std::uint32_t N>
    void foo(myclass<N>& obj) {
    std::cout << "Called foo<" << std::hex << N << ">\n";
    obj.print(); // 调用特化模板的成员函数(如果有)
    }

    // 通用函数:根据运行时输入调用特定模板
    void invoke_function(std::string_view hex_input) {
    // 将字符串解析为 16 进制数值
    std::uint32_t num;
    std::stringstream ss;
    ss << std::hex << hex_input;
    ss >> num;
    myclass<num> obj;
    foo(obj);

    }

    // 主程序
    int main() {
    try {
    invoke_function("1f0"); // 调用 foo<1f0>
    invoke_function("2f0"); // 调用 foo<2f0>
    invoke_function("3f0"); // 抛出异常
    } catch (const std::exception& e) {
    std::cerr << "Error: " << e.what() << '\n';
    }
    return 0;
    }

    ```

    以后要添加新的,直接写个对应的特化模板就完事
    abc612008
        50
    abc612008  
       1 天前
    @bluearc 这是 gpt 写的吗?模板参数丢个运行时变量进去怎么可能编译的过。
    chendl111
        51
    chendl111  
       1 天前
    优雅没有任何意义
    ysc3839
        52
    ysc3839  
       1 天前 via Android
    @abc612008 用 stringstream 解析确实可能是 GPT 写的,但是直接传参(非运行时,代码里写的是字符串字面量)再转成常量是可行的,constexpr 就可以。
    woniu7
        53
    woniu7  
       1 天前
    优雅没有任何意义
    ysc3839
        54
    ysc3839  
       1 天前 via Android
    @bluearc @abc612008
    是我搞错了,用这种方式还是得把可能的取值写出来,不然模板不会实例化,还是避免不了 if else 。
    这么写只能让代码更直观,因为数值不是写在函数名内。
    mayli
        55
    mayli  
       1 天前
    我觉得你最好还是给个最小化的例子说下参数怎么不同,不然没法准备参数…
    ysc3839
        56
    ysc3839  
       20 小时 46 分钟前 via Android
    @bluearc @abc612008
    不对,模板可以解决,需要手动把所有可能的取值写出来,例如:
    test<0x1f0, 0x1f1, 0x1f2>(i);

    如果能接受非标准扩展的话,有种办法可以把取值加到宏列表里:
    #include <cstdio>
    #include <cstdlib>

    template<int N>
    void func();

    template<typename T = void>
    void test(int n)
    {
    puts("test() failed!");
    }

    template<int N, int... ints>
    void test(int n)
    {
    printf("test<%i>(%i)\n", N, n);
    if (n == N) {
    func<N>();
    } else {
    test<ints...>(n);
    }
    }

    #define PUSHVAL _Pragma("push_macro(\"VALUES\")")
    #define POPVAL _Pragma("pop_macro(\"VALUES\")")

    #define VALUES 2
    template<>
    void func<2>()
    {
    puts("func<2>()");
    }

    PUSHVAL
    #undef VALUES
    #define VALUES POPVAL VALUES, 3
    template<>
    void func<3>()
    {
    puts("func<3>()");
    }

    PUSHVAL
    #undef VALUES
    #define VALUES POPVAL VALUES, 5
    template<>
    void func<5>()
    {
    puts("func<5>()");
    }

    PUSHVAL
    #undef VALUES
    #define VALUES POPVAL VALUES, 7
    template<>
    void func<7>()
    {
    puts("func<7>()");
    }

    int main(int argc, char** argv)
    {
    if (argc != 2) {
    printf("usage: %s <int>\n", argv[0]);
    return 1;
    }

    int n = (int)strtoul(argv[1], nullptr, 0);
    printf("n = %i\n", n);

    test<VALUES>(n);
    }

    上述方法在 gcc 中有效,msvc 无效。
    bluearc
        57
    bluearc  
       14 小时 9 分钟前
    @ysc3839 #56 我的,让 chatgpt 生成后感觉差不多就发了,上班摸鱼修改了下:
    ```
    #include <cstdint>
    #include <functional>
    #include <iostream>
    #include <map>
    #include <memory>
    #include <sstream>
    #include <stdexcept>
    #include <string>

    class BaseClass {
    public:
    virtual ~BaseClass() = default;
    virtual void print() = 0;
    };

    std::map<uint32_t, BaseClass *> reg;

    template <std::uint32_t N> struct myclass : public BaseClass {
    void print() override {
    throw std::logic_error("Error: myclass<" + std::to_string(N) +
    "> is not specialized.");
    }
    };

    template <uint32_t N> void register_classes() {
    reg[N] = new myclass<N>();
    if constexpr (N > 0) {
    register_classes<N - 1>();
    }
    }

    template <> struct myclass<0x1ff> : public BaseClass {
    void print() override { std::cout << "Specialized myclass<1f0>\n"; }
    };
    template <> struct myclass<0x1fa> : public BaseClass {
    void print() override { std::cout << "Specialized myclass<2f0>\n"; }
    };

    // 通用模板函数
    void foo(BaseClass *obj) {
    obj->print(); // 调用特化模板的成员函数(如果有)
    }

    // 通用函数:根据运行时输入调用特定模板
    void invoke_function(std::string_view hex_input) {
    // 将字符串解析为 16 进制数值
    std::uint32_t num;
    std::stringstream ss;
    ss << std::hex << hex_input;
    ss >> num;
    foo(reg[num]);
    }

    // 主程序
    int main() {
    register_classes<0xfff>();

    try {
    invoke_function("1ff");
    invoke_function("1fa");
    invoke_function("3f0");
    } catch (const std::exception &e) {
    std::cerr << "Error: " << e.what() << '\n';
    }
    for (auto &pair : reg) {
    delete pair.second;
    }
    return 0;
    }
    ```

    还是写特化模板就行,编译时给编译器传一个参数:-ftemplate-depth=4096 ;
    Nimrod
        58
    Nimrod  
       12 小时 48 分钟前
    运行时值到类型的分发,具体的,这里就是`N` 到 `void foo<N>(myclass<N>&)`。
    这里存在一个问题需要楼主表达清楚,对应类型的参数是如何构造出来的。
    这里为作简化`void foo<N>()`

    核心逻辑是做一次`int V`到`std::integral_constant<int, V>`的映射,再用 lambda 包装一下原本的`foo<N>`使得能用上推断出来的类型。

    ```cpp
    #define DISPATCH(VALUE) \
    case VALUE: \
    return f(std::integral_constant<int, VALUE>{});


    // trampline function
    template <typename F>
    auto dispatcher(F&& f, int value) {
    switch(value) {
    DISPATCH(0x1f0)
    DISPATCH(0x1f1)
    DISPATCH(0x1f2)
    default:
    throw std::runtime_error("Unregistered value");
    }
    }

    void foo_wrapper(int num) {
    dispatcher([](auto type){
    constexpr auto v = decltype(type)::value;
    foo<v>();
    }, num);
    }
    ```
    这里,`dispatcher`是可以完全可以复用的。
    [Demo]( https://godbolt.org/z/5TdevE4We)

    剩下的就是手动`DISPATCH(N)`来注册你需要的值,也可以使用 BOOST_PP_REPEAT 来生成代码。
    [Demo]( https://godbolt.org/z/1893bzEs8)

    运行时的类型分发可以参考我的这篇博客,https://nimrod.blog/posts/cpp-elegant-ways-to-map-runtime-values-to-types
    abc612008
        59
    abc612008  
       9 小时 41 分钟前
    @ysc3839 #56 你这个调用一次 func 的时间是 O(n)的吗..难道要从 0 开始试
    ```
    ❯ ./a.out 7
    n = 7
    test<2>(7)
    test<3>(7)
    test<5>(7)
    test<7>(7)
    func<7>()
    ```
    ysc3839
        60
    ysc3839  
       9 小时 38 分钟前
    @abc612008 手动写 if else 也是一个个试,你去掉 test 中的 printf ,看编译后的代码,和手写 if else 是一致的。
    abc612008
        61
    abc612008  
       9 小时 28 分钟前
    @bluearc #57 都用上 map 了那不如这样:

    ```
    #include <iostream>
    #include <functional>

    constexpr int maxN = 100;

    class Foo {
    public:
    static void invoke(int i) {
    _mapping[i](i);
    }

    private:
    static std::function<void(int)> _mapping[maxN];

    template <int N>
    struct FooHelper {
    FooHelper() {
    _mapping[N] = [](int n) {
    std::cout << "Foo<" << N << "> (" << n << ")" << std::endl;
    };
    }
    };
    template <int N>
    struct Initer : Initer<N-1> {
    FooHelper<N> _foo;
    };
    static Initer<maxN> _initer;
    };
    template<>
    struct Foo::Initer<0> {};
    std::function<void(int)> Foo::_mapping[maxN];
    Foo::Initer<maxN> Foo::_initer;

    int main(){
    int n;
    std::cin>>n;
    Foo::invoke(n);
    }
    ```

    虽然感觉也挺丑的
    ysc3839
        62
    ysc3839  
       8 小时 17 分钟前
    @bluearc @abc612008
    去请教了一下群友,成功通过 Stateful Template Meta Programming (STMP) 实现了仅在模板特化时声明一次取值,后续自动根据所有可能的取值生成代码:
    https://godbolt.org/z/GqY7ozrh7

    代码基于 https://ykiko.me/zh-cn/articles/646812253/ 该文章修改,未进行优化,仅仅是展示可行性。
    这代码说多不多、说少不少,感觉并不是非常优雅,看情况选择是否使用吧。
    gcc 及 msvc 都可用,gcc 会报 warning ,msvc 不会。
    abc612008
        63
    abc612008  
       3 小时 16 分钟前
    @ysc3839 #62 已经有点复杂到一般人看不懂了... 其实取决于场景,甚至可以直接丢 dll/so 里然后运行时加载库直接找对应的符号(
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2684 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 01:46 · PVG 09:46 · LAX 17:46 · JFK 20:46
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.