Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Retrieve value out of "cascading ifs" fold expression

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.

like image 211
Vittorio Romeo Avatar asked Sep 27 '17 14:09

Vittorio Romeo


1 Answers

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.

like image 140
HolyBlackCat Avatar answered Sep 16 '22 17:09

HolyBlackCat