12

I have a function taking a variadic parameter pack and at the beginning I want to check that all elements compare equal. Can I somehow use the new C++17 fold-expressions to write that succinctly as a one-liner? I was thinking

template<typename... Args>
void func (Args... args)
{
    ASSERT ((args == ...));

    // more code here...
}

but this doesn't work, as it compiles to code that first properly compares the last two arguments, but then compares the third-last argument to the result of the first comparison, which is a bool. What use-case could this type of fold expression possibly have (similar for args < ...)? Is there any chance I can avoid writing a dedicated recursive template to do this?

max66
  • 65,235
  • 10
  • 71
  • 111
levzettelin
  • 2,600
  • 19
  • 32

3 Answers3

31

The reason that doesn't work, unfortunately, is due to the fact that boolean operators don't chain in C++ like they do in other languages. So the expression:

a == (b == c)

(what your fold-expression would expand to) would compare a to either true or false, nothing to do with what b or c actually are. I was hoping the operator<=> would add chaining but apparently that part was dropped.

The fixes are that you have to break up the comparisons:

(a == b) && (b == c)

Of course that doesn't lend itself to folding very well, but you could instead compare everything to the first element:

(a == b) && (a == c)

Which is to say:

((a0 == args) && ... )

At that point, we just need to be able to pull out the first element. No problem, that's obviously what lambdas are for:

template <class... Args>
constexpr bool all_equal(Args const&... args) {
    if constexpr (sizeof...(Args) == 0) {
        return true;
    } else {
        return [](auto const& a0, auto const&... rest){
            return ((a0 == rest) && ...);
        }(args...);
    }
}
yuyoyuppe
  • 1,582
  • 2
  • 20
  • 33
Barry
  • 286,269
  • 29
  • 621
  • 977
9

As suggested by Piotr Skotnicki, a simple solution is separate the first argument from the followings and check it with using && as fold operator

By example, the following function that return true if all arguments are equals

template <typename A0, typename ... Args>
bool foo (A0 const & a0, Args const & ... args)
 { return ( (args == a0) && ... && true ); } 

Unfortunately this can't work with an empty list of arguments

std::cout << foo(1, 1, 1, 1) << std::endl; // print 1
std::cout << foo(1, 1, 2, 1) << std::endl; // print 0
std::cout << foo() << std::endl;           // compilation error

but you can add the special empty argument foo()

bool foo ()
 { return true; }

If, for some reason, you can't split the args in a a0 and the following args?

Well... you can obviously use the preceding foo() function (with special empty version)

template<typename... Args>
void func (Args... args)
{
    ASSERT (foo(args));

    // more code here...
}

or you can use the C++17 fold expression with comma operator and assignment as in the following bar()

template <typename ... Args>
bool bar (Args const & ... args)
 {
   auto a0 = ( (0, ..., args) );
   return ( (args == a0) && ... && true ); 
 }

Observe the initial zero in a0 assignment that permit the use of this solution also with an empty list of arguments.

Unfortunately, from the preceding auto a0 assignment I get a lot of warnings ("expression result unused", from clang++, and "left operand of comma operator has no effect", from g++) that I don't know how to avoid.

The following is a full working example

#include <iostream>

template <typename A0, typename ... Args>
bool foo (A0 const & a0, Args const & ... args)
 { return ( (args == a0) && ... && true ); }

bool foo ()
 { return true; }

template <typename ... Args>
bool bar (Args const & ... args)
 {
   auto a0 = ( (0, ..., args) );
   return ( (args == a0) && ... && true ); 
 }

int main ()
 {
   std::cout << foo(1, 1, 1, 1) << std::endl; // print 1
   std::cout << foo(1, 1, 2, 1) << std::endl; // print 0
   std::cout << foo() << std::endl;           // print 1 (compilation error
                                              //          witout no argument
                                              //          version)

   std::cout << bar(1, 1, 1, 1) << std::endl; // print 1
   std::cout << bar(1, 1, 2, 1) << std::endl; // print 0
   std::cout << bar() << std::endl;           // print 1 (no special version)
 }

-- EDIT --

As pointed by dfri (thanks!), for and empty args... pack, the values for the following folded expressions

( (args == a0) && ... )

( (args == a0) || ... )

are, respectively, true and false.

So return instruction of foo() and bar() can be indifferently written

 return ( (args == a0) && ... && true );

or

 return ( (args == a0) && ... );

and this is true also in case sizeof...(args) == 0U.

But I tend to forget this sort of details and prefer to explicit (with the final && true) the empty-case value.

max66
  • 65,235
  • 10
  • 71
  • 111
  • Note that the value for a unary fold over an empty pack, using specifically `&&`, is `true` (conversely, `false` for `||`), so `bar(...)`'s return (`return ( (args == a0) && ... && true );`) needn't have the trailing `true`, and can be simplified to `return ( (args == a0) && ...);`. Also, _"Observe the initial zero in `a0` assignment that permit the use of this solution also with an empty list of arguments."_ <-- isn't the initial value simply to shift `a0` one "element" as compared to the `args` pack (to allow comparing adjacent elements)? – dfrib Oct 18 '17 at 13:00
  • @dfri - Regarding `&&` and `true`: yes, I know (now) this, but I tend to forget this sort of details; I think it's better (for me and other forgetful people like me) explicit the default value (and i suppose the compiler optimize and delete the final `&& true`); but you're right and maybe I add a comment. Regarding the "initial zero"... sorry but I don't understand (my fault, I suppose) what do you mean). – max66 Oct 18 '17 at 15:12
  • Regarding the second part: it's probably me that should say "my fault", as I don't really understand the sentence I quoted: what is the significance of the `0` value w.r.t. being able to call `bar(...)` with an empty pack (`bar()`)? As I understand, the initial `0` value (which may as well be `-42`) is used mainly so that the fold expression compares each "adjacent element" of the initial pack (such that `a0` itself is a "shifted-by-one" parameter pack). But I feel that I'm probably missing the logic for this one. – dfrib Oct 18 '17 at 15:31
  • @dfri - To understand the logic of the initial zero, try to compile my full example removing it (using `auto a0 = ( (..., args) );`). From the `bar()` empty call (last `std::cout` row) you should obtain an error similar to "error: variable has incomplete type 'void'". And now think how is expanded an empty pack. With the initial zero, is expanded as `auto as = 0;`, that is correct and compile. Without the initial zero, is expanded as `auto as = ;`, that is wrong and give an error. – max66 Oct 18 '17 at 15:37
  • I see, thanks; so we're actually after the type inferred from the `0` literal (whereas we never use it's value) in case of an empty pack. In such case, could `std::nullopt` from `` be preferable (for semantics)? – dfrib Oct 18 '17 at 16:16
  • @dfri - Is `std::nullopt` preferable? Good question. From the practical point of view no, because my compilers are to old and don't support it (OK: I'm joking; isn't a good motivation). From the semantical point of view... sorry but I'm not a standard guru, so I suspend the judgment. – max66 Oct 18 '17 at 16:39
  • Thanks for your time and answers, the last question is probably just one of preference. – dfrib Oct 18 '17 at 17:07
0

Here is how I do it in gcl library :

template <auto ... values>
constexpr static auto equal_v = []() consteval {
    static_assert(sizeof...(values) > 0, "gcl::mp::value_traits::equal_v : no arguments");
    constexpr auto first_value = std::get<0>(std::tuple{values...});
    static_assert(
            (std::equality_comparable_with<decltype(values), decltype(first_value)> && ...),
            "gcl::mp::value_traits::equal_v : cannot compare values");
    return ((values == first_value) && ...); 
}();

or replacing static_assert by concept requirement :

template <typename ... Ts>
concept are_equality_comparable = requires(Ts ... values)
{
    { 
        std::conditional_t<(std::equality_comparable_with<decltype(std::get<0>(std::tuple{values...})), decltype(values)> && ...), std::true_type, std::false_type>{}
    } -> std::same_as<std::true_type>;
};

template <auto ... values>
    requires(are_equality_comparable<decltype(values)...>)
constexpr static auto equal_v = []() consteval {
    static_assert(sizeof...(values) > 0, "gcl::mp::value_traits::equal_v : no arguments");
    constexpr auto first_value = std::get<0>(std::tuple{values...});
   
    return ((values == first_value) && ...); 
}();
Guss
  • 762
  • 4
  • 20