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

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

  •  2
     
  •   aqtata · 11 天前 · 7531 次点击

    运行时从外部读取一个 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  
       11 天前   ❤️ 1
    std::map
    liuguangxuan
        2
    liuguangxuan  
       11 天前   ❤️ 1
    模板。
    blu10ph
        3
    blu10ph  
       11 天前   ❤️ 2
    你是否在找:工厂模式/策略模式?

    感觉你描述的还比较清晰,把你发的这段内容交给 AI,一会就能出来代码~
    yuzii
        4
    yuzii  
       11 天前   ❤️ 1
    std::vector<std::function<void(int)>> foos;
    aqtata
        5
    aqtata  
    OP
       11 天前
    @yuzii 参数不同,而且注册也得写几百行,感觉和 if/else 差不多。
    aqtata
        6
    aqtata  
    OP
       11 天前
    @lichao 注册函数?感觉和 if/else 差不多,也要写上百行。
    aqtata
        7
    aqtata  
    OP
       11 天前
    @liuguangxuan 展开讲讲。。
    YakumoZi
        8
    YakumoZi  
       11 天前   ❤️ 1
    @liuguangxuan 模板是编译时计算,他这个要求运行时读取,应该做不到吧?
    zwy100e72
        9
    zwy100e72  
       11 天前   ❤️ 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  
       11 天前   ❤️ 7
    对于无反射的静态编译型语言,这种情况 if 和 switch 是最清晰的,因为阅读代码的人只要看三个 if 就知道这坨是干什么的,也非常容易定位和修改,用其他自以为“优雅”的解决方案很大概率只是满足了自己苦了他人。
    Metatron7
        11
    Metatron7  
       11 天前   ❤️ 1
    不想写 ifelse 就 LLM 生成得了
    zwy100e72
        12
    zwy100e72  
       11 天前   ❤️ 1
    写了一个超省略的 demo ,供参考 https://godbolt.org/z/ox7K6sM1e
    leonshaw
        13
    leonshaw  
       11 天前 via Android   ❤️ 2
    直接 switch ,case 用宏包一下,再用工具生成。
    强行搞可以把函数导出,运行时查符号。
    guanzhangzhang
        14
    guanzhangzhang  
       11 天前
    怎么感觉你是在搞啥协议解析,前面是 msgID 对应不同的解析函数,去处理后面的 data 部分
    geelaw
        15
    geelaw  
       11 天前   ❤️ 2
    wow 这个问题完全 under-specified 。第一个问题:你知道要调用什么函数了,可你怎么制造不同类型的参数传入之?

    但楼主不应该尝试回答我的第一个问题,而是应该直接说自己实际上要解决的问题,而不是来问自己觉得可行的一半解决方法的另一半。
    chashao
        16
    chashao  
       11 天前   ❤️ 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  
       11 天前
    这样写是最好的
    sapphire
        18
    sapphire  
       11 天前   ❤️ 1
    这种情况适宜从整体项目 build 的角度考量,有工程上的设计和考量。如果语言本身没有反射机制,一般用脚本或其他自定义工具,在 Pre-build 阶段生成函数的桩,同时对参数和调用场景做必要的安全检查。这样对于函数实现代码,只要做好必要的标注,就和 C#没什么区别了。
    MoYi123
        19
    MoYi123  
       11 天前   ❤️ 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  
       11 天前   ❤️ 1
    查表 map<key,func>
    HtPM
        21
    HtPM  
       11 天前   ❤️ 1
    宏定义
    qq135449773
        22
    qq135449773  
       11 天前   ❤️ 1
    这种需求本来难以简化的,Map<key, func>已经是不错的选择了。

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

    GET_HEX_REGITER(a)
    ```
    这是用宏来写的 感觉应该能满足吧 能不用映射就不用映射
    min
        37
    min  
       11 天前
    这个问题你找不到大模型可以问吗?
    netabare
        38
    netabare  
       11 天前 via iPhone
    用表驱动和高阶函数会比较好
    nicebird
        39
    nicebird  
       11 天前
    宏定义吧
    nicebird
        40
    nicebird  
       11 天前
    另外现在直接用 copilot 生成就行了,也不用写啥代码
    minami
        41
    minami  
       11 天前
    正解是 libffi
    chandlerbing9317
        42
    chandlerbing9317  
       11 天前
    分支比较多改成表驱动就好了
    ETiV
        43
    ETiV  
       11 天前 via iPhone
    都是 16 进制范围内就用偏移做呗
    LuJyKa
        44
    LuJyKa  
       11 天前
    把这堆函数导出成 Dll ,然后用 GetProcAddress 直接传字符串得到函数地址,然后把参数地址传进去。
    vituralfuture
        45
    vituralfuture  
       11 天前 via Android
    写个脚本,在构建阶段生成文件参与编译
    liuidetmks
        46
    liuidetmks  
       11 天前 via iPhone
    @jcharr 要 runtime 吧
    FrankFang128
        47
    FrankFang128  
       11 天前
    选中代码,让 AI 消除 if else
    198plus
        48
    198plus  
       11 天前
    建议代码生成,用“写代码的代码”维护代码,我记得 rust 编译器里面就有这种东西,用 python 脚本生成 rust 代码
    bluearc
        49
    bluearc  
       11 天前   ❤️ 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  
       11 天前
    @bluearc 这是 gpt 写的吗?模板参数丢个运行时变量进去怎么可能编译的过。
    chendl111
        51
    chendl111  
       11 天前
    优雅没有任何意义
    ysc3839
        52
    ysc3839  
       10 天前 via Android
    @abc612008 用 stringstream 解析确实可能是 GPT 写的,但是直接传参(非运行时,代码里写的是字符串字面量)再转成常量是可行的,constexpr 就可以。
    woniu7
        53
    woniu7  
       10 天前
    优雅没有任何意义
    ysc3839
        54
    ysc3839  
       10 天前 via Android
    @bluearc @abc612008
    是我搞错了,用这种方式还是得把可能的取值写出来,不然模板不会实例化,还是避免不了 if else 。
    这么写只能让代码更直观,因为数值不是写在函数名内。
    mayli
        55
    mayli  
       10 天前
    我觉得你最好还是给个最小化的例子说下参数怎么不同,不然没法准备参数…
    ysc3839
        56
    ysc3839  
       10 天前 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  
       10 天前
    @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  
       10 天前
    运行时值到类型的分发,具体的,这里就是`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  
       10 天前
    @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  
       10 天前
    @abc612008 手动写 if else 也是一个个试,你去掉 test 中的 printf ,看编译后的代码,和手写 if else 是一致的。
    abc612008
        61
    abc612008  
       10 天前
    @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  
       10 天前
    @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  
       9 天前
    @ysc3839 #62 已经有点复杂到一般人看不懂了... 其实取决于场景,甚至可以直接丢 dll/so 里然后运行时加载库直接找对应的符号(
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   722 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 89ms · UTC 21:06 · PVG 05:06 · LAX 13:06 · JFK 16:06
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.