2
\$\begingroup\$

This is a follow-up question for A recursive_count_if Function For Various Type Arbitrary Nested Iterable Implementation in C++, A recursive_count_if Function with Specified value_type for Various Type Arbitrary Nested Iterable Implementation in C++ and A recursive_count_if Function with Automatic Type Deducing from Lambda for Various Type Arbitrary Nested Iterable Implementation in C++. As G. Sliepen's answer Don't try to deduce the predicate's parameter type and Quuxplusone's answer mentioned I think the user of this API should have to tell the function explicitly how many "levels" to unwrap downward, I am trying to implement another version recursive_count_if template function with unwrap level parameter. Thanks to Quuxplusone for providing the hint of using if constexpr to achieve this.

template<std::size_t unwrap_level, class T1, class T2> requires (is_iterable<T1>)
auto recursive_count_if(const T1& input, const T2 predicate)
{
    if constexpr (unwrap_level > 1)
    {
        return std::transform_reduce(std::cbegin(input), std::cend(input), std::size_t{}, std::plus<std::size_t>(), [predicate](auto& element) {
            return recursive_count_if<unwrap_level - 1>(element, predicate);
            });
    }
    else
    {
        return std::count_if(input.begin(), input.end(), predicate);
    }
}

The used is_iterable concept is as below. I keep it in order to get nicer error messages if user accidentily try to apply this recursive_count_if() on something that can not be iterated over.

template<typename T>
concept is_iterable = requires(T x)
{
    *std::begin(x);
    std::end(x);
};

Some test cases are as below. With this version recursive_count_if, you can always use auto keyword in the input lambda parameter.

//  std::vector<std::vector<int>> case
std::vector<int> test_vector{ 1, 2, 3, 4, 4, 3, 7, 8, 9, 10 };
std::vector<decltype(test_vector)> test_vector2;
test_vector2.push_back(test_vector);
test_vector2.push_back(test_vector);
test_vector2.push_back(test_vector);

// use a lambda expression to count elements divisible by 3.
int num_items1 = recursive_count_if<2>(test_vector2, [](auto& i) {return i % 3 == 0; });
std::cout << "#number divisible by three: " << num_items1 << '\n';

// std::deque<std::deque<int>> case
std::deque<int> test_deque;
test_deque.push_back(1);
test_deque.push_back(2);
test_deque.push_back(3);

std::deque<decltype(test_deque)> test_deque2;
test_deque2.push_back(test_deque);
test_deque2.push_back(test_deque);
test_deque2.push_back(test_deque);

// use a lambda expression to count elements divisible by 3.
int num_items2 = recursive_count_if<2>(test_deque2, [](auto& i) {return i % 3 == 0; });
std::cout << "#number divisible by three: " << num_items2 << '\n';

std::vector<std::vector<std::string>> v = { {"hello"}, {"world"} };
auto size5 = [](auto& s) { return s.size() == 5; };
auto n = recursive_count_if<2>(v, size5);
std::cout << "n:" << n << std::endl;

//  std::vector<std::vector<std::vector<int>>> case
std::vector<decltype(test_vector2)> test_vector3;
test_vector3.push_back(test_vector2);
test_vector3.push_back(test_vector2);
test_vector3.push_back(test_vector2);
std::cout << recursive_count_if<1>(test_vector3,
    [test_vector2](auto& element)
    {
        return std::equal(element.begin(), element.end(), test_vector2.begin());
    }) << std::endl;

A Godbolt link is here.

All suggestions are welcome.

The summary information:

\$\endgroup\$
1
\$\begingroup\$
  1. I would expect the predicate not have a generic T-type. F for function, or Pred for predicate would be normal.

  2. I suggest not copying the predicate internally. Standard algorithms generally only copy it until they finally use it, and then only use that one single copy.

    To that end, wrap it in a std::reference_wrapper if it isn't already wrapped, and don't mark it const so mutable predicates still work.

    if constexpr (std::is_same_v<F, std::unwrap_reference_t<F>>)
        return recursive_count_if<unwrap_level>(input, std::ref(predicate));
    
  3. Retaining your previous behavior for unwrap_level == 0 would be interesting.

    Otherwise, make it an error.

  4. You could give unwrap_level a useful default. Like 1 or maybe 0 if you implement the previous suggestion.

  5. Not sure how far you want to go, but it would be nice if the function was more SFINAE-friendly if the template-arguments don't work together.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.