9

Let's assume that I want to create my own lambda-based switch with the following syntax:

auto s = make_switch(std::pair{0, []{ return 0;   }},
                     std::pair{1, []{ return 50;  }},
                     std::pair{2, []{ return 100; }});

assert( s(0) == 0   );
assert( s(1) == 50  );
assert( s(2) == 100 );

I would like to use a fold expression in order to have a terse implementation that does not require recursion. The idea is to generate something similar to a bunch of nested if statements:

if(x == 0) return 0;
if(x == 1) return 50;
if(x == 2) return 100;

I would like to write this:

// pseudocode
template <typename... Pairs>
auto make_switch(Pairs... ps)
{
    return [=](int x)
    {
        ( if(ps.first == x) return ps.second(), ... );
    };
}

The code above doesn't work as if(...){...} is not an expression. I then tried to use the && operator:

template <typename... Pairs>
auto make_switch(Pairs... ps)
{
    return [=](int x)
    {
        return ((ps.first == x && ps.second()), ...);
    };
}

This does compile, but returns the result of ps.first == x && ps.second(), which is a bool and not the int value that I want.

I would like some kind of operator that is a combination between the comma operator and &&: it should evaluate and evaluate to the right hand side of the operator iff the left hand side evaluates to true.

I cannot think of any technique that would allow me to implement this in such a way I can get ps.second()'s return value and propagate it to the caller of the lambda returned by make_switch.

Is it possible to implement this kind of "cascading ifs" pattern with a fold expression? I would like to evaluate only as many expressions as required until a matching branch is found.

Barry
  • 286,269
  • 29
  • 621
  • 977
Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416
  • Since all your `typename Pairs::second_type...` are different, I think you can do it only with recursion. – j6t Sep 27 '17 at 14:32
  • @j6t: but all `decltype(ps.second)()` are the same. My intention is generating a set of `if` statements with a *fold expression* – Vittorio Romeo Sep 27 '17 at 14:35
  • They are not the same. Each of the lambdas in your example invocation is a different type. They cannot be the same because each lambda's operater() implementation is different even though they look like they have the same signature. – j6t Sep 27 '17 at 15:19
  • @j6t: sorry, I screwed up my comment. I meant that they all return `int`. (i.e. `decltype(ps.second())` is the same) – Vittorio Romeo Sep 27 '17 at 15:45

3 Answers3

17

I'm surprised it wasn't suggested yet:

template <typename ...Pairs> auto make_switch(Pairs ...ps)
{
    return [=](int x)
    {
        int ret;
        ((x == ps.first && (void(ret = ps.second()), 1)) || ...)
            /* || (throw whatever, 1) */ ;
        return ret;
    };
}

(try it online)

It requires an additional variable, but it seems the only alternatives are recursion and a wrapper class with an overloaded binary operator, and both look less elegant to me.

The short-circuiting of || is used to stop the function when a match is found.

(For the above code GCC 7.2 gives me warning: suggest parentheses around '&&' within '||'. Probably a bug?)

Edit:

Here's a version generalized for any types: (credits to @Barry for suggesting std::optional)

template <typename InputType, typename ReturnType, typename ...Pairs> auto make_switch(Pairs ...ps)
{
    /* You could do
     *   using InputType  = std::common_type_t<typename Pairs::first_type...>;
     *   using ReturnType = std::common_type_t<decltype(ps.second())...>;
     * instead of using template parameters.
     */
    
    return [=](InputType x)
    {
        std::optional<ReturnType> ret /* (default_value) */;
        ( ( x == ps.first && (void(ret.emplace(std::move(ps.second()))), 1) ) || ...)
            /* || (throw whatever, 1) */;
        return *ret;
    };
}

(try it online)

I decided to use template parameters for parameter and return types, but you can deduce them if you want.

Note that if you decide to not have a default value nor throw, then passing an invalid value to the switch will give you UB.

Community
  • 1
  • 1
HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • Interesting. Any way to generalize this to `T` instead of `int`, supporting types that only provide construction? – Vittorio Romeo Sep 27 '17 at 15:46
  • @VittorioRomeo I have some ideas, will do the code in a moment. – HolyBlackCat Sep 27 '17 at 15:55
  • @HolyBlackCat - what about `( (ret = ps.second(), ps.first == x) || ... );`? – max66 Sep 27 '17 at 17:17
  • @max66 It's shorter, but less efficient (unless the compiler optimizes it, and I wouldn't want to rely on it). – HolyBlackCat Sep 27 '17 at 17:25
  • @HolyBlackCat - yes... it's a bad idea; impose a lot of assignments that should be avoided. – max66 Sep 27 '17 at 17:27
  • This answer is brilliant, wish I could upvote twice. I used this technique to implement a dynamic dispatch function using cascading `if (dynamic_cast) ...`. The only gotcha I ran into was that [`std::common_type_t` does not preserve references](https://stackoverflow.com/questions/21975812/should-stdcommon-type-use-stddecay), so if you want to have a `using` declaration instead of a template parameter and return a reference you have to play some games with `std::is_reference` and `std::decay`. Still, it works great – thanks @HolyBlackCat! – deltacrux Apr 04 '19 at 22:38
0

This is not possible. In order to use a fold expression, you would need to define a binary operator on your Pairs.

In your case, such a binary operator cannot exist since:

  • it would need to be statefull (i.e. capture x since it compares Pairs::first with x)
  • and an operator must be either a (i) non-static member function or a (ii) non-member function.

Furthermore:

  • (i) a non-static member operator implicitly takes this as a first argument, and you could not make this a pointer to a Pairs or a derived of Pairs;
  • (ii) a non-member function could not capture the value of x.
YSC
  • 38,212
  • 9
  • 96
  • 149
0

I think the solution from HolyBlackCat is better but... what about using a sum?

template <typename ... Pairs>
auto make_switch (Pairs ... ps)
 {
   return [=](int x)
    { return ( (ps.first == x ? ps.second() : 0) + ... ); };
 }

Unfortunately, works only for types where the sum is defined.

max66
  • 65,235
  • 10
  • 71
  • 111