c++recursionstd-ranges

Solution or alternative for recursive view


We want to implement a tree of items:

       +----------------------+
       |   Item (interface)   |
       +----------------------+
       | + getName(): string  |-----------+
       +----------------------+           |
                    ^                     |
                    |                     |
             +------+------+              |
             |             |              |
   +-------------+      +------------+    |
   | SimpleItem  |      |   Group    |<>--+
   +-------------+      +------------+
                        | +add(Item) |
                        | +all()     |
                        +------------+

The Group::all() function shall return a flattened recursive view of all items including self, as pairs of: {recursion level, item}.

An implementation using ranges::views fails, as the return type of all, depends on the level (a view of a view of items is not the same type as a view of items).

So this code doesn't compile:

auto all(size_t level = 0) const {
    return views::concat(
        views::single(std::pair<size_t, std::shared_ptr<const Item>>(level, shared_from_this())), 
        views::join(items | 
            views::transform([level](const auto& item) {
                auto group = std::dynamic_pointer_cast<Group>(item);
                if (group == nullptr) {
                    return views::single(std::pair{level + 1, item});
                }
                else {
                    return group->all(level + 1);
                }
            })
        )
    );
}

Code: https://compiler-explorer.com/z/fdc8rsWae

Any idea how to make the all function work and return the desired flattened view of all recursive items?

* Note: the code uses ranges-v3, as we use concat, which is added to std::ranges only in C++26. Solutions may rely either on std::ranges or on ranges-v3.


Solution

  • A function needs to have a single return type, and every time concat is called recursively the type changes, you need to type-erase the result at some point, like the return of the lambda.

    one way to type-erase ranges is ranges::any_view.

    using erased_view = ranges::any_view<std::pair<size_t, std::shared_ptr<const Item>>,ranges::category::input>;
    auto all(size_t level = 0) const {
        return views::concat(
            views::single(std::pair<size_t, std::shared_ptr<const Item>>(level, shared_from_this())), 
            views::join(items | 
                views::transform([level](const auto& item)->erased_view {
                    auto group = std::dynamic_pointer_cast<Group>(item);
                    if (group == nullptr) {
                        return views::single(std::pair{level + 1, item});
                    }
                    else {
                        return group->all(level + 1);
                    }
                })
            )
        );
    }
    

    godbolt demo