3

For a C++17 restricted project I would like to have a standalone implementation of C++20 std::views::join(). I am aware of the existence of range-v3 but unfortunately the person in charge is unwilling to include further 3rd party libraries.

My aim is the write the C++17 equivalent of this (Implementation) --> solved see below

std::vector<std::vector<int>> data{{1,2},{1,2,3},{1,2,3,4}};
for(const auto & element : std::views::join(data)){
    std::cout << element << "\n";
}

and the more difficult part this (Godbolt)

std::vector<std::vector<std::vector<int>>> data{{{1,2},{3,4}},{{5,6,7}}};
auto nested_join = std::views::join(std::views::join(data));
for(const auto element : nested_join){
   std::cout << element << "\n";
}

I want to additionally emphasis the nesting of calls (std::views::join(std::views::join(data))), as they are the current road-block. To be more specific

How can I design my join_view class to be nestable [/stackable, e.g. join_view(join_view(T))] [, while preserving the functionality which std::views::join provides (i.e. optionally modifiable elements Godbolt)?](refined/edited question)

Update: A std::views::join object can first be created for a range of ranges (e.g. a std::vector<std::vector<T>> godbolt), as, I suspect, the standard intends it to be comparable, in the performance sense, with writing the explizit nested loops. I think restricting one self to this basecase may lead to a better implementation. My version provided down below fullfills this, is though design wise flawed, resulting in segfaults for nesting of join_view.


I have successfully implemented a C++17 solution for the first part (Implementation), class code is provided at the end. The join_view wrapper class works by holding a reference to the nested object (which may or may not be const) and provides begin() and end() function to allow a range based for loop. These return an internal iterator (I think it fulfills the LegacyInputIterator requirements), for which the required operators (++,*,==,!=) are implemented.

Now lets consider my unsuccessful attempts to implement the second part.

  1. Let's just try if I have written a super code that works as well for nesting the join_view construction. Nope. For a triple nested vector std::vector<std::vector<std::vector<int>>> and a twice applied join_view instead of a int as element type I receive a std::vector<int> (Implementation). If we take a look at the types of the nested construction auto deeper_view = join_view(join_view(data_deeper)); this expands in C++ Insights to join_view<std::vector<...>> deeper_view = join_view(join_view<std::vector<...>>(data_deeper)); which is obviously a sign of an issue?

  2. I then tried changing all calls of the std::begin() and std::end() to their $.begin() counterpart, since these are the one defined for the join_view wrapper. Unfortunately this did also not help but now the C++ Insights output is unreadable and I cant follow it anymore.

Now I am no longer sure that this could even work therefore I am asking the question from above: How can I redesign my join_view class to be nestable?

I am aware of the following stackoverflow questions regarding std::views::join [join view how, join boost problem, join string_view problem, join compilation issue], but these do not consider implementation. I have as well tried understanding and reading the implementation of ranges-v3 join wrapper, which has provided to be difficult.

Current partially working status of standalone join_view wrapper:

template<typename T>
class join_view{
private:
    T & ref_range;
    using outer_iterator = decltype(ref_range.begin());
    using inner_iterator = decltype((*ref_range.begin()).begin());

public:
    join_view(T & range) : ref_range{range} {}

    class iterator{
    private:
        outer_iterator outer;
        inner_iterator inner;
    public:
        iterator(outer_iterator outer_, inner_iterator inner_): outer{outer_}, inner{inner_} {}

        auto& operator*(){
            return *inner;
        }
    
        auto& operator++(){
            ++inner;
            if(inner != (*outer).end()){
                return *this;
            }
            ++outer;
            inner = (*outer).begin();
            
            return *this;
        }
        auto operator==(const iterator & other){
            return outer == other.outer;
        }

        auto operator!=(const iterator & other){
            return outer != other.outer;
        }
    };

    auto begin(){
        return iterator(ref_range.begin(),  (*ref_range.begin()).begin());
    }

    auto end(){
        return iterator(ref_range.end(),{});
    }
};
joscao
  • 35
  • 7

1 Answers1

1

You need to implement a recursive class template to achieve your goal. Here is a working quick and dirty prototype.

#include <cassert>
#include <iostream>
#include <type_traits>
#include <vector>

template <class T>
struct is_container : public std::false_type {};

// you'll need to declare specializations for the containers you need.
template <class T, class Alloc>
struct is_container<std::vector<T, Alloc>> : public std::true_type {};

// basic definition for our view
template <typename T, typename = void>
struct join_view;

// specialization for non-container types
template <typename T>
struct join_view<T, std::enable_if_t<!is_container<T>::value>> {
    using contained_type = T;
    using outer_const_iterator = const T*;

    using const_iterator = const T*;

    join_view(const T& t) : t_(t) {}

    const_iterator begin() const { return &t_; }

    const_iterator end() const { return begin() + 1; }

    const T& t_;
};

// specialization for containers
template <typename Container>
struct join_view<Container, std::enable_if_t<is_container<Container>::value>> {
    using contained_type = typename Container::value_type;
    using outer_const_iterator = typename Container::const_iterator;

    using inner_container_type = join_view<contained_type>;
    using inner_const_iterator = typename inner_container_type::const_iterator;

    friend inner_container_type;

    class const_iterator {
        friend join_view;
        friend inner_container_type;

       public:
        const_iterator() = default;
        const_iterator(const const_iterator&) = default;
        const_iterator(const_iterator&&) = default;

        const_iterator& operator=(const const_iterator&) = default;
        const_iterator& operator=(const_iterator&&) = default;

       private:
        const_iterator(const Container* container,
                       const outer_const_iterator& outer,
                       const inner_const_iterator& inner)
            : container_(container), outer_(outer), inner_(inner) {}

        const_iterator(const Container* container, outer_const_iterator outer)
            : container_(container), outer_(outer) {
            assert(outer_ == container_->end());
        }

       public:
        const_iterator& operator++() {
            if (outer_ == container_->end()) return *this;
            if (++inner_ != inner_container_type{*outer_}.end()) return *this;

            for (;;) {
                if (++outer_ == container_->end()) break;

                inner_ = inner_container_type{*outer_}.begin();
                if (inner_ != inner_container_type{*outer_}.end()) break;
            }
            return *this;
        }

        bool operator==(const const_iterator& other) const {
            if (outer_ == other.outer_) {
                if (outer_ == container_->end()) return true;
                return inner_ == other.inner_;
            }
            return false;
        }

        bool operator!=(const const_iterator& other) const {
            return !(*this == other);
        }

        const auto& operator*() const { return *inner_; }

       private:
        const Container* container_ = nullptr;
        outer_const_iterator outer_;
        inner_const_iterator inner_;
    };

    join_view(const Container& container) : outer_(container) {}

    const_iterator begin() const {
        return {&outer_, outer_.begin(),
                inner_container_type{*(outer_.begin())}.begin()};
    }

    const_iterator end() const { return {&outer_, outer_.end()}; }

    const Container& outer_;
};

template <typename T>
auto make_join_view(const T& t) {
    return join_view<T>(t);
}

int main() {
    static_assert(is_container<std::vector<int>>::value);
    static_assert(!is_container<int>::value);

    int test_int = 42;

    for (auto x : make_join_view(test_int)) std::cout << x << std::endl;
    std::cout << std::endl;

    std::vector<int> v{1, 2, 3};
    for (const auto& x : make_join_view(v)) std::cout << x << std::endl;
    std::cout << std::endl;

    std::vector<std::vector<int>> vv{{1}, {2, 3}, {4, 5, 6}};
    for (const auto& x : make_join_view(vv)) std::cout << x << std::endl;
    std::cout << std::endl;

    std::vector<std::vector<std::vector<int>>> vvv{
        {{1}, {2, 3}, {4, 5, 6}}, {{11}, {22, 33}, {44, 55, 66}}};
    for (const auto& x : make_join_view(vvv)) std::cout << x << std::endl;
}

It's very rough, as it only handles very basic traversals, but should work for most container types.

I think is_container<> can be rewritten to check for the presence of Container::const_iterator. That would allow join_view to work on any container, but also on any view.

Note: make sure your implementation uses the name const_iterator, that's absolutely imperative for this scheme to work.

You can play with the code here: https://godbolt.org/z/ejecq5MhE

Update:

Here is an updated type traits utility for detecting containers. It relies on the presence of an in-class iterator type called iterator, and its twin that detect constant containers and views.

template <class T, typename = void>
struct is_container : public std::false_type {};

template <typename  T>
struct is_container<T, std::void_t<typename T::iterator>> : public std::true_type {};

template <class T, typename = void>
struct is_const_container : public std::false_type {};

template <typename  T>
struct is_const_container<T, std::void_t<typename T::const_iterator>> : public std::true_type {};

Update 2: Added support for empty sub-views.

Michaël Roy
  • 6,338
  • 1
  • 15
  • 19
  • Thank you, the recursive implementation seems like the thing I was missing. I will do some testing later one. On a first glance it seems like this implementation is restricted to having a const reference element type (or copy), in your implementation the loop variable `x`, and a non const reference is not possible. Could you confirm that? Since std::views::join allows a modification of the elements, I at least strive to keep that. I just realized that I may have forgotton to mention that in my question. Sorry about that. – joscao Feb 20 '23 at 09:09
  • I'm not proficient with std::view yet, so 9I didn't check for that. The code can easily be changed to a non-const view, but a complete implementation would require quite a bit of extra declarations and meta-programming gymnastic. How would you insert/remove elements, though? In the end, it may be better to create separate separate specializations for non-const views. – Michaël Roy Feb 20 '23 at 09:42
  • Oh yeah, inserting/removing elements would be quite something, not sure it makes even sense in the context that I planed on using it. I will accept your answer, as it solves my main issue. Additionally I learned that my requirements are not entirely clear, so I will refine them and return if I have additional questions. Thank you again. – joscao Feb 20 '23 at 09:54
  • @joscao I've updated the class is_container with something a bit more practical. – Michaël Roy Feb 20 '23 at 20:19
  • So I started playing around with it and it seems like it can not deal with an nested empty vector [godbolt](https://godbolt.org/z/KnvMYPvjh). I did not immediately find it where it produces the segfault. – joscao Feb 21 '23 at 10:36
  • But I am wondering more about if the idea to have a `join_view` for a scalar type is even a good idea, design wise. If one takes a look at `std::views::join` the first possible range that can be joined is a range of ranges (e.g. `std::vector>`) [godbolt](https://godbolt.org/z/T947s5n97). I assume this is mainly done for performance reasons, as it should be in the same order as writing the loops explizitly. I fear that is a desing flaw of this solution, I will update the question accordingly. – joscao Feb 21 '23 at 10:37
  • You're probably right. Views become useful when you combine them. – Michaël Roy Feb 21 '23 at 16:06
  • @joscao As I said, it is a rough prototype .. I've updated the code and links for empty sub views support. – Michaël Roy Feb 21 '23 at 16:28