Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

cppitertools: How to combine iter::enumerate and iter::filter?

Consider this example (using cppitertools):

#include <vector>
#include <iostream>

#include <cppitertools/enumerate.hpp>
#include <cppitertools/filter.hpp>

int
main()
{
    std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8};

    auto f = iter::enumerate(v) | iter::filter([](auto& i) { return std::get<1>(i) > 4; });

    for (auto&& [i, e]: f) {
        std::cout << i << ", " << e << '\n';
    }
}

What I'm trying to do, is filtering results from iter::enumerate (trying to get only values that are > 4). Unfortunately, this snippet doesn't compile (g++ 7.4):

Error:

error: use of deleted function ‘std::optional<iter::impl::EnumIterYield<long unsigned int, int&> >& std::optional<iter::impl::EnumIterYield<long unsigned int, int&> >::operator=(std::optional<iter::impl::EnumIterYield<long unsigned int, int&> >&&)’

...

error: use of deleted function ‘std::_Enable_copy_move<true, false, true, false, _Tag>& std::_Enable_copy_move<true, false, true, false, _Tag>::operator=(std::_Enable_copy_move<true, false, true, false, _Tag>&&) [with _Tag = std::optional<iter::impl::EnumIterYield<long unsigned int, int&> >]’

Expected result:

4, 5
5, 6
6, 7
7, 8

When I remove iter::enumerate(v) and do filtering only on plain vector, it works:

auto f = v | iter::filter([](auto& i) { return i > 4; });

for (auto&& i: f) {
    std::cout << i << '\n';
}

Prints:

5
6
7
8

Can I filter results of iter::enumerate()? I'm suspecting I got the lambda function in iter::filter wrong.

like image 374
Andrej Kesely Avatar asked Nov 29 '25 16:11

Andrej Kesely


1 Answers

iter::enumerate returns an instance of type iter::impl::Enumerable. Its iterator, once dereferenced, returns iter::impl::EnumIterYield<unsigned long, int&> which is a class type which derives from std::pair<unsigned long, int&>, but also redeclares its own data members, whose index (first) data member stores the value index, while element (second) is a reference to the actual value from the container (see int&). This is fine, as we want to be able to modify the content of the container through an iterator, and also avoid unnecesary copies:

std::vector<int> v{1, 2, 3};
auto e = iter::enumerate(v);
std::get<1>(*e.begin()) = 5;

This is how the class is implemented:

template <typename Index, typename Elem>
using EnumBasePair = std::pair<Index, Elem>;

template <typename Index, typename Elem>
class EnumIterYield : public EnumBasePair<Index, Elem> {
    using BasePair = EnumBasePair<Index, Elem>;
    using BasePair::BasePair;

public:
    typename BasePair::first_type index = BasePair::first;
    typename BasePair::second_type element = BasePair::second;
};

However, the iter::impl::EnumIterYield type does not declare its own copy/move-assignment operators, which makes objects of this class non-copy-assignable (this will be important later) -- the implicitly declared copy-assignment operator will be therefore defined as deleted if either index or element are of reference types (or const).

Going further, by filtering the result of iter::enumerate with iter::filter, another object is created, this time of type iter::impl::Filtered. Its iterator stores a so called DerefHolder named _item. DerefHolder is supposed to hold inside a dereferenced value of the wrapped iterator:

// DerefHolder holds the value gotten from an iterator dereference
// if the iterate dereferences to an lvalue references, a pointer to the
//     element is stored
// if it does not, a value is stored instead

That is, DerefHolder is specialized for the case when the template argument is a reference type, in which case it stores a (copy-assignable) pointer. For non-reference types, an actual value is stored in an std::optional (referred to as item_p_).

Having a specialization of DerefHolder for reference types, it is possible to operate on the filtered container values directly using iter::impl::Filtered::Iterator.

However, a dereferenced iter::impl::Enumerable::Iterator does not result in a reference type. As said before, it is a non-copy-assignable iter::impl::EnumIterYield<unsigned long, int&>, which does not trigger the specialization, and instead it causes the compiler to fall back to the primary template of DerefHolder. Its reset() function, invoked when the first value that should not be filtered-out is found, has the following definition:

void reset(T&& item) {
    item_p_ = std::move(item);
}

That is, it invokes std::optional<iter::impl::EnumIterYield<unsigned long, int&>>::operator=, which in turn tries to copy-assign the problematic EnumIterYield (holding a reference type member without an explicitly defined copy/move-assignment operators), failing to do so. This is what the error message reports:

error: use of deleted function

I see no reason to try to copy-assign the stored value, as it can equally well be re-created, so I suggest to change the implementation to, instead, copy(move)-construct the item stored in std::optional:

void reset(T&& item) {
    item_p_.emplace(std::move(item));
}
like image 174
Piotr Skotnicki Avatar answered Dec 02 '25 05:12

Piotr Skotnicki



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!