1

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!

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
Pablo
  • 557
  • 3
  • 16

3 Answers3

3

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.

Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • 1
    sorry, i delete my last comment by mistake. I write it again: Thank you very much for your answer! It is going to take me a while to really understand the code, but now I have some clear code to keep improving my (few right now) variadic templates skill. Also, I would like to thank you for your c++20 share. As developer, I am looking forward for beginning to work with c++20 and its new features, specially ranges, so any new learning is welcomed. – Pablo Jan 28 '21 at 22:48
  • 1
    By the way, I unknew the "sizeof...(Bs)" construction that allows to rid off of the horrible tuple hack I used to calculate the number of arguments in the function. – Pablo Jan 28 '21 at 22:59
  • 1
    @Pablo `sizeof...` is really nice! I get to use it way to rarely in everyday life :-) – Ted Lyngmo Jan 28 '21 at 23:01
  • @max66 Thank you! I liked yours too! It took me long enough to make a C++17 solution and was too tired to reboot my brain to make a C++14 version like you did. Yet, I'm stuck with C++14 at work :-) – Ted Lyngmo Jan 28 '21 at 23:38
  • You both are genius! Where did you learn to program like that? I'm still find problems everytime I use c++03 templates. Thanks to you both for sharing! – Pablo Jan 28 '21 at 23:54
  • @max66 ... but that's a fold expression? – Ted Lyngmo Jan 28 '21 at 23:55
  • 1
    @TedLyngmo - You're right; sorry: I've only transferred the fold expression. Forget it. – max66 Jan 28 '21 at 23:56
  • @Pablo I have severe problems with templates myself. I've spent the last few years looking at amazing template magic here on SO so the little I do know about templates, I got from here. – Ted Lyngmo Jan 28 '21 at 23:56
2

What about (C++17) as follows?

template <typename... Args>
int error_code (Args... args)
 {
   int ret = 0;
   int val = 1;

   ( (args || ret ? 0 : ret = val, ++val), ... );

   return ret;
 }

In C++11 and C++14 a little (little!) more typewriting is required.

template <typename... Args>
int error_code (Args... args)
 {
   using unused = int[];

   int ret = 0;
   int val = 1;

   (void)unused{ 0, (args || ret ? 0 : ret = val, ++val)... };

   return ret;
 }
max66
  • 65,235
  • 10
  • 71
  • 111
  • Thank you very much for your answer! It is going to take me a while and a whole debuger session to understand how this works, but at least now I have some solid code to keep going deeper into variadic templates. – Pablo Jan 28 '21 at 22:46
2

Since you know your arguments are all going to be converted to a bool, it's better to not use variadic arguments at all:

inline int error_code(std::initializer_list<bool> args) {
    int index = std::find(args.begin(), args.end(), false) - args.begin();
    if (index == args.size()) return 0;
    return 1 + index;
}

// Either directly call the above `error_code({ true, true, false, ... })`
// Or if you must have a parameter pack

template<typename... Args>
int error_code(Args... args) {
    std::initializer_list<bool> v{ args... };
    int index = std::find(v.begin(), v.end(), false) - v.begin();
    if (index == sizeof...(args)) return 0;
    return index + 1;
    // If you have both functions, simply have: return error_code({ args... });
}

The compiler seems to optimise it similarly to your variadic solution (and it even works in C++11).


Here's a more fun solution that uses C++17 fold expressions:

template<typename... Args, int... I>
int error_code_impl(Args... args, std::integer_sequence<int, I...>) {
    int result = 0;
    ([&result](bool arg){
        if (!arg) result = I + 1;
        return arg;
    }(args) && ...);
    // Can also be written without the lambda as something like:
    // ((args ? true : ((result = I + 1), false)) && ...);
    return result;  
}

template<typename... Args>
int error_code(Args... args) {
    std::make_integer_sequence<int, sizeof...(args)> indices;
    return error_code_impl<Args...>(indices, args...);
}
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
Artyer
  • 31,034
  • 3
  • 47
  • 75
  • Really cool! At a first glance, I almost was tempted to use variadic functions from good old plain c language. Your solution has remember me to that, but yours gets better using a "std::initializer_list" instead of the va_start, va_end... creepy keywords. I really like your first solution, until now it has the best readibility and it is easy to understand by anyone – Pablo Jan 28 '21 at 23:42