4

My colleague wrote a very long switch-case function as below:

void func(int type, int a, int b) {
    std::string str = "hello";
    switch (type) {
        case 1: {
            func1(a, b);
            break;
        }
        case 2: {
            func2(a, b, str);
            break;
        }
        // hundreds of cases...
    }
}

I want to change this terrible funciton because it has too many cases.

Good new is that each case only invokes one function, which must use a and b as its first and second parameters. Bad news is that some of them may need the third parameter.

I'm thinking if I can use a std::map<int, std::function<void(int, int, ...)>> to store all of functions in the cases but it's not working. It doesn't seem that std::function could accept a variadic function.

It there some other way to do so?

Yves
  • 11,597
  • 17
  • 83
  • 180
  • Where do the additional arguments come from? For those you could use a lambda to creat a function taking two ints. But ultimately a big switch will be more performant. You could use an array of function pointers instead of a map. – Ben Jul 17 '21 at 03:34
  • @Ben I reedited my question. The additional arguments are all defined in the function `func`. – Yves Jul 17 '21 at 03:36
  • I was more curious which functions have how many extra args? Is it just one or two or so most take one or more extra args? – Ben Jul 17 '21 at 03:37
  • @Ben There are only two kinds of functions: one is `void(int, int)`, the other is `void(int, int, ???)`. For now, `???` is always `std::string`. – Yves Jul 17 '21 at 03:39
  • @Ben I can't tell exactly how many functions taking two arguments and how many functions taking three functions. Because this may change any time. – Yves Jul 17 '21 at 03:43
  • Got it. I don't know why you have this function (a parser/interpreter maybe?). It seems a bit weird, but not crazy. And unless you need to change the mapping at runtime, the original version with a `switch` is going to be *way* faster than anything with `std:map`. – Ben Jul 17 '21 at 12:44

2 Answers2

2

std::any will be your friend. Together with some wrapper class and a template function within the wrapper class, to hide the any cast, it will be some how more intuitive.

And it will give you additional possibilities.

Please see:

#include <iostream>
#include <map>
#include <string>
#include <any>
#include <utility>

class Caller
{
    std::map<int, std::any> selector;
public:
    Caller() : selector() {}

    Caller(std::initializer_list<std::pair<const int, std::any>> il) : selector(il) {}
    template<typename Function>
    void add(int key, Function&& someFunction) { selector[key] = std::any(someFunction); };

    template <typename ... Args>
    void call(int key, Args ... args) {
        if (selector.find(key) != selector.end()) {
            std::any_cast<std::add_pointer_t<void(Args ...)>>(selector[key])(args...);
        }
    }
};

// Some demo functions
void a(int x) { 
    std::cout << "a\t" << x << '\n'; 
};
void b(int x, int y) {
    std::cout << "b\t" << x << '\t' << y << '\n';
};
void c(int x, int y, std::string z) {
    std::cout << "c\t" << x << '\t' << y << '\t' << z << '\n';
};
void d(std::string s, int x, int y, int z) {
    std::cout << "d\t" << s << '\t' << x << '\t' << y << '\t' << z << '\n';
};


// Definition of our caller map (using initializer list)
Caller caller{
    {1, a},
    {2, b},
    {3, c} };

int main() {

    // Some demo calls
    caller.call(1, 1);
    caller.call(2, 1, 2);
    caller.call(3, 1, 2, std::string("3"));

    // Adding an additional function
    caller.add(4, d);

    // And call this as well.
    caller.call(4, std::string("ddd"), 1,2,3);
    return 0; 
}
A M
  • 14,694
  • 5
  • 19
  • 44
1

You can wrap into lambdas like

void func(int type, int a, int b) {    
    std::string str = "hello";

    std::map<int, std::function<void(int, int)>> m {
        {1, [](int a, int b) { func1(a, b); } },
        {2, [str](int a, int b) { func2(a, b, str); } },
        ...
    };

    m[type](a, b);
}
songyuanyao
  • 169,198
  • 16
  • 310
  • 405
  • I reedited my question. I'm thinking if your method is suitable for my case... – Yves Jul 17 '21 at 03:37
  • Closer. But you see I don't want to put so many codes in `func`. I'm trying to move `std::map` out of `func`. I mean I want to define the `std::map` out of `func` so that I can simply write things like `m[type](a, b)` in `func`. – Yves Jul 17 '21 at 03:45
  • If there is no other way, I think I may need two `map`, one is to store functions with two parameters, and the other is to store functions with three parameters. – Yves Jul 17 '21 at 03:47
  • @Yves I can't think of other ways to do that, we have to specify the signature at compile-time in advance. And yes, you can define two `map`s if the type of the 3rd parameter is fixed. – songyuanyao Jul 17 '21 at 03:49
  • You can write `dummy_func1(int, int, str)` that just ignores the `str` and calls `func1(int, int)` with the first two arguments. – Nathan Pierson Jul 17 '21 at 05:31
  • This has turned an O(1) function into an O(n log n) function with O(n log n) allocations. This will be far far slower with no real benefit. You could make the `std::map` `static` if it isn't changing, but still, I don't see the benefit. – Ben Jul 17 '21 at 12:46
  • Gentlem, maybe my solution below may help . . . – A M Jul 17 '21 at 13:05