Implementing operator less for arrays using fold expressions
Asked Answered
L

2

7

I am playing around with fold expression in c++17 with the latest clang++. I tried to implement the less operator for an array using this which I want to use for fixed size strings.

Here is where I got to. Is there a better way to do this, especially avoiding assigning the index in the expression?

Compiling this using "clang++ test_fold_expr_less.cpp -o test_fold_expr_less -std=c++1z" and the output is here.

prompt$ ./test_fold_expr_less
=== less ===
0
1
0
0
1
0
0
0
0
1
1
1

#include <iostream>
#include <utility>

std::uint64_t arr1[8] = {1, 7, 2, 4, 8, 9, 3, 6};
std::uint64_t arr2[8] = {1, 7, 2, 4, 8, 9, 3, 6};
std::uint64_t arr3[8] = {1, 7, 2, 5, 8, 9, 3, 6};
std::uint64_t arr4[8] = {1, 7, 2, 3, 8, 9, 3, 6};

struct less_t
{
    template < typename T, std::size_t N, std::size_t... I >
    bool impl(T const (& lhs)[N], T const (& rhs)[N], std::index_sequence < I... >) const
    {
      std::size_t i{0};
      if (((i = I, (lhs[I] < rhs[I]) ? true : lhs[I] != rhs[I]) || ...))
          return lhs[i] < rhs[i];
      else
          return false;
    }

    template < typename T, std::size_t N >
    bool operator () (T const (& lhs)[N], T const (& rhs)[N]) const
    {
        return impl(lhs, rhs, std::make_index_sequence < N >());
    }
};

int main()
{
    std::cout << "=== less ===" << std::endl;
    less_t const less{};
    std::cout << less(arr1, arr2) << std::endl;
    std::cout << less(arr1, arr3) << std::endl;
    std::cout << less(arr1, arr4) << std::endl;

    std::cout << less(arr2, arr1) << std::endl;
    std::cout << less(arr2, arr3) << std::endl;
    std::cout << less(arr2, arr4) << std::endl;

    std::cout << less(arr3, arr1) << std::endl;
    std::cout << less(arr3, arr2) << std::endl;
    std::cout << less(arr3, arr4) << std::endl;

    std::cout << less(arr4, arr1) << std::endl;
    std::cout << less(arr4, arr2) << std::endl;
    std::cout << less(arr4, arr3) << std::endl;
}
Luminescence answered 7/7, 2015 at 16:29 Comment(8)
Why don't you just use a loop? I could understand why one would want to use a fold-expression for tuples, but why for simple arrays?Isomerous
bool less; auto equal = ((lhs[I] == rhs[I] || (less = lhs[I] < rhs[I], false)) && ...); return !equal && less; is slightly shorter.Isomerous
I think there is a fundamental reason why a side-effect is required in the fold-expression: lexicographical comparison needs to bail out of the computation in two cases (less and greater). The only sane way to bail out in fold-expr is short-circuiting (SC). SC only allows two values (bail out / continue), where we have three cases (greater, less, equal). By mapping the three cases to the boolean for SC, we cannot restore the info we need to return (less / greater or equal).Isomerous
@Isomerous I just realized that your solution is similar to my answer, but smarter. Why don't you post it as an answer?Amora
@DanielFrey It is still ugly :( While the equal = IMO makes sense, neither do I like the side-effect nor the cryptic !equal && less.Isomerous
I second dyp's comment re loops vs fold-expressions. If you are concerned about loop overhead, the compiler can and will unroll loops on std::array (since the size is known at compile time).Ashleyashli
Thanks for the answers/comments. I am essentially trying to use fold expressions to unroll loops.Luminescence
Clearly we need a trinary operator fold.Frigid
I
8

I'll start with some observations and hypotheses concerning the use of fold-expressions here.

Essentially, we want to write a lexicographical comparison via a single fold-expression. We want to bail out of the comparisons once we can determine the result. Using a unary left fold

(... OP comparison)

evaluates to

(  ( (comparison(0) OP comparison(1)) OP comparison(2) )...  )

The only ways to not evaluate the comparison(I) are throwing an exception in one of the comparisons executed earlier and short-circuiting. I don't think using exceptions is a good idea here. So I'll try short-circuiting. This requires OP to be either || or &&, and the expression to the left must evaluate to a bool that tells the computation whether or not to continue:

(  ( (comparison(0) && comparison(1)) && comparison(2) )...  )

comparison(N) -> bool // continue?

In lexicographical comparison, we continue evaluating if the current elements of the lhs and rhs are equal. So the value of comparison(I) must be lhs[I] == rhs[I]:

#define comparison(I) (lhs[I] == rhs[I])

The result of the whole fold-expression then tells us whether or not both sequences are entirely equal:

auto equal = (... && comparison(I));

However, by doing this, we lose the information about whether lhs[I] < rhs[I] or lhs[I] > rhs[I] was the reason we stopped the comparison. We can get this information out of the expression by using a side-effect:

#define comparison(I) (less = lhs[I] < rhs[I], lhs[I] == rhs[I])

bool less;
auto equal = (... && comparison(I));

At this point, we know either equal == true or we can use the value last stored to less to determine the overall result:

return !equal && less;

(If lhs == rhs then !(lhs < rhs), so we return false. Otherwise, lhs != rhs and we use the result stored in less. Therefore, less doesn't need to be initialized if we have at least one element in the array.)

There is still room for improvement: We can perform the assignment to less, and with it the value computation of lhs[I] < rhs[I] only when we need it, that is, only when lhs[I] != rhs[I]. Using another short-circuit:

#define comparison(I) (lhs[I] == rhs[I] || (less = lhs[I] < rhs[I], false))
//                                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

I'd consider this quite cryptic. The value of the underlined part is always false due to the use of a comma-expression. Since false is the neutral element of ||, the whole || (..) only performs a side-effect but doesn't change the value of comparison(I), but this side-effect is only performed if the left-hand side of || yields false.

One last improvement can be achieved by recognizing that if we never assign to less, we know that lhs == rhs and we can return false.

Combined, we get:

bool less = false;
auto equal = (... && (lhs[I] == rhs[I] || (less = lhs[I] < rhs[I], false)));
(void)equal; // we don't need you
return less;

Complete example:

#include <iostream>
#include <utility>

struct loud
{
    std::uint64_t v;
    loud(int x) : v(x) {}
    static auto& opEqual() { static int v = 0; return v; }
    static auto& opLess() { static int v = 0; return v; }
    friend bool operator==(loud l, loud r) { return opEqual()++, l.v == r.v; }
    friend bool operator<(loud l, loud r) { return opLess()++, l.v < r.v; }

    static void print_stats(std::ostream& o) {
        o << "operator< " << opLess() << " operator== " << opEqual();
    }
    static void reset_stats() {
        opEqual() = opLess() = 0;
    }
};

loud arrs[][8] = {
 {1, 7, 2, 4, 8, 9, 3, 6}
 ,{1, 7, 2, 4, 8, 9, 3, 6}
 ,{1, 7, 2, 5, 8, 9, 3, 6}
 ,{1, 7, 2, 3, 8, 9, 3, 6}
};

struct less_t
{
    template < typename T, std::size_t N, std::size_t... I >
    bool impl(T const (& lhs)[N], T const (& rhs)[N], std::index_sequence < I... >) const
    {
            bool less = false;
    auto equal = (... && (lhs[I] == rhs[I] || (less = lhs[I] < rhs[I], false)));
    (void)equal; // we don't need you
    return less;
    }

    template < typename T, std::size_t N >
    bool operator () (T const (& lhs)[N], T const (& rhs)[N]) const
    {
        return impl(lhs, rhs, std::make_index_sequence < N >());
    }
};

template<class T, int N>
void test(T const (&lhs)[N], T const (&rhs)[N])
{
    auto const stdres = std::lexicographical_compare(lhs, lhs+N, rhs, rhs+N);
    loud::reset_stats();
    auto const foldres = less_t{}(lhs, rhs);
    std::cout << (stdres == foldres) << " -- ";
    loud::print_stats(std::cout);
    std::cout << "\n";
}

int main()
{
    std::cout << std::boolalpha;
    std::cout << "=== less ===" << std::endl;
    for(auto& lhs : arrs)
           for(auto& rhs : arrs)
                test(lhs, rhs);
}

Output -- note I do not print the result of the fold-expression-function, but I compare that result to the result of std::lexicographical_compare. true therefore means both algorithms produced the same result.

=== less ===
true -- operator< 0 operator== 8
true -- operator< 0 operator== 8
true -- operator< 1 operator== 4
true -- operator< 1 operator== 4
true -- operator< 0 operator== 8
true -- operator< 0 operator== 8
true -- operator< 1 operator== 4
true -- operator< 1 operator== 4
true -- operator< 1 operator== 4
true -- operator< 1 operator== 4
true -- operator< 0 operator== 8
true -- operator< 1 operator== 4
true -- operator< 1 operator== 4
true -- operator< 1 operator== 4
true -- operator< 1 operator== 4
true -- operator< 0 operator== 8
Isomerous answered 8/7, 2015 at 19:22 Comment(5)
That is an elegant solution. I wonder if it could be modified to work also for the case where the operator== is not defined, i.e. we can only do x<y or y<x. and we do not want to explicitly implement == as !(a<b || b<a), as it would be expensive.Kaka
@Kaka (... && (!(less = lhs[I] < rhs[I]) && !rhs[I] < lhs[I])), on coliruDependency
About the stats of your loud struct, when you're running the test class. Why do you not print_stats after the lexicographical_compare at first and then reset_stats and print_stats of the fold results to compare them ? Another point is that the count printed after each operator== prints the index of the last position that was compared but not the truth number of equals elements, is that an intentional behavior ? (eg. based on your third test case, the 3 firsts elements are equals and the 4th is not (4 vs 5), so operator== should be 3 rather than 4 isn't it ?)Lammond
@Isomerous Regarding your testing loop, you can remove your second line in your louds array because of comparing lines with themselves. So each 2 first tests are the same in each group of 4 tests produced by rhs-loopLammond
@caleth, indeed. I came up myself with something very similar, based on ||. I posted it in the answer below, in case it may help somebody.Kaka
K
0

A more flexible way, which also works for arrays of type A, where the class A implements only the operator<, is the following:

struct A
{
    int x;
    constexpr bool operator<(const A& b) const { return x < b.x;}
};

template <typename T, size_t N, size_t...Is>
static constexpr bool lt(const array<T,N>& a, const array<T,N>& b, std::index_sequence<Is...>&&)
{
    // For every I in Is the pack expression we first computes a[I]<b[I] and store the result in res as a side effect, 
    // then if the result is false, we check for b[I]<a[I].
    // It terminates as soon as any one of the two expressions a[I]<b[I] or b[I]<a[I] is true.
    // If I+1==N, this is the last element of the array and we can skip the check b[I]<a[I]
    // The final result is stored in res.
    bool res = false;
    (... || ((res = a[Is]<b[Is])==true || ((Is+1 < N) && b[Is]<a[Is]) ) );
    return res;
}

template <typename T, size_t N>
static constexpr bool lt(const array<T,N>& a, const array<T,N>& b)
{
    return lt(a, b, std::make_index_sequence<N>{});
}

// adding some compile-time sanity checks
using array_t = array<A, 4>;
static_assert(lt(array_t{1,2,3,3}, array_t{1,2,3,4}));
static_assert(lt(array_t{1,2,2,4}, array_t{1,2,3,4}));
static_assert(lt(array_t{1,1,3,4}, array_t{1,2,3,4}));
static_assert(lt(array_t{0,2,3,4}, array_t{1,2,3,4}));
static_assert(!lt(array_t{1,2,3,4}, array_t{1,2,3,4}));
static_assert(!lt(array_t{1,2,3,4}, array_t{1,2,3,3}));
static_assert(!lt(array_t{1,2,3,4}, array_t{1,2,2,4}));
static_assert(!lt(array_t{1,2,3,4}, array_t{1,1,3,4}));
static_assert(!lt(array_t{1,2,3,4}, array_t{0,2,3,4}));
Kaka answered 31/5, 2024 at 10:0 Comment(2)
you don't need the false || in the fold, and the test (Is+1 < N) only saves one compare when the sequences are equal.Dependency
Saving the last compare is the intent. Thanks for the suggestion on false. I thought it was needed for arrays of zero length.Kaka

© 2022 - 2025 — McMap. All rights reserved.