Search code examples
c++c++14c++17variadic-templatesfold-expression

Improving a variadic template function using new c++14 / c++17 features


I am newbie to variadic templates, still I managed to program some code in c++11 using it, but I still feel sour about result because it lacks expressivity.

The issue is to implement a function that accept several bool conditions (from 1 to whatever) and returns an integer code indicating in what place is the first "false" argument, or 0 if all of them are true.

e.g. "error_code(true, true, true);" must return 0
e.g. "error_code(true, true, false);" must return 3
e.g. "error_code(true, false, false);" must return 2
e.g. "error_code(false, false, false);" must return 1

My current code stands for (live link to coliru: http://coliru.stacked-crooked.com/a/1b557f2819ae9775):

#include <tuple>
#include <iostream>

int error_impl(bool cond)
{
    return cond;
}

template<typename... Args>
int error_impl(bool cond, Args... args)
{
    return cond ? 1 + error_impl(args...) : 0;
}

template<typename... Args>
int error_code(Args... args)
{
    constexpr std::size_t size = std::tuple_size<std::tuple<Args...>>::value + 1;
    return (error_impl(args...) + 1) % size;
}

int main()
{
    auto e1 = error_code(true, true, true);
    auto e2 = error_code(true, true, false);
    auto e3 = error_code(true, false, false);
    auto e4 = error_code(false, false, false);
    std::cout << std::boolalpha;
    std::cout << "(true, true, true)==0 -> " << (e1 == 0) << "\n";
    std::cout << "(true, true, false)==3 -> " << (e2 == 3) << "\n";
    std::cout << "(true, false, false)==2 -> " << (e3 == 2)<< "\n";
    std::cout << "(false, false, false)==1 -> " << (e4 == 1)<< "\n";

    auto e5 = error_code(true, true, true, true);
    auto e6 = error_code(true, true, true, false);
    auto e7 = error_code(true, true, false, false);
    auto e8 = error_code(true, false, false, false);
    auto e9 = error_code(false, false, false, false);
    std::cout << "(true, true, true, true)==0 -> " << (e5 == 0) << "\n";
    std::cout << "(true, true, true, false)==4 -> " << (e6 == 4) << "\n";
    std::cout << "(true, true, false, false)==3 -> " << (e7 == 3) << "\n";
    std::cout << "(true, false, false, false)==2 -> " << (e8 == 2)<< "\n";
    std::cout << "(false, false, false, false)==1 -> " << (e9 == 1)<< "\n";
}

I wonder where this "error_code()" function can be improved using new unroll features from c++14 / c++17, so it gains in expressivity and uses less than 3 functions.

Any help will be grateful welcomed!


Solution

  • C++17 with folding:

    template<class... Bools>
    constexpr unsigned error_code(Bools... Bs) {
        unsigned rv = 1;
        (void) ((rv += Bs, !Bs) || ...);
        return rv % (sizeof...(Bs) + 1);
    }
    

    Unasked for so it's just a bonus - the same idea, C++20:

    constexpr unsigned error_code(auto... Bs) {
        unsigned rv = 1;
        (void) ((rv += Bs, !Bs) || ...);
        return rv % (sizeof...(Bs) + 1);
    }
    

    Explanation:

    • The first part of the fold expression contains two parts separated by a ,. The result of the left part is discarded and the result of such an expression is the rightmost part, !Bs.

      (rv += Bs, !Bs)
      
    • The second part || ... is where the folding (or unfolding) comes in. The first expression is copy/pasted repeatedly until there are no more arguments in the pack. For true, false, true it becomes:

      (rv += 1, !true) || (rv += 0, !false) || (rv += 1, !true)
      

      or

      (rv += 1, false) || (rv += 0, true) || (rv += 1, false)
      
    • Short-circuit evaluation kicks in. When the built-in1 operator || has a true on the left side, the right side is not evaluated. That's why only one of the rv += 1's is done in this example. The (rv += 0, true) stops the evaluation so only this is evaluated:

      (rv += 1, false) || (rv += 0, true)
      
    • The final rv % (sizeof...(Bs) + 1); is to take care of the case where no false values are found and we should return 0. Example:

      unsigned rv = 1;
      (rv += 1, !true) || (rv += 1, !true) || (rv += 1, !true);
      
      // rv is now 4, and sizeof...(Bs) == 3, so:
      
      4 % (3 + 1) == 0
      
    • Why (void)?
      Compilers like to warn about what they see as unused expressions. A carefully placed (void) tells it that we don't care, so it keeps the compiler silent.


    1 - This does not apply to user defined operators.