This HowTo documents how to write a view using the standard library and some helpers from SeqAn.
Difficulty | Difficult |
Duration | 120 min |
Prerequisite tutorials | C++ Concepts,Ranges |
Recommended reading | |
- Note
- Some of the links from this HowTo only resolve in the developer documentation because they refer to entities from the seqan3::detail namespace. We recommend you open this tutorial from the developer documentation.
Motivation
We have introduced "views" in Ranges. You can do many things with the views provided by the standard library and those shipped with SeqAn, but in certain situations you will want to define your own view. This page will teach you the basics of defining your own view.
What makes a view?
A view is a type of std::ranges::range
that also models std::ranges::view
. The additional requirements of std::ranges::view
can be vaguely summarised as "not holding any own data" or at least not holding data that is relative in size to the number of elements in the view (e.g. a vector cannot be a view, because its size in memory depends on the number of elements it represents).
A simple example of a view is std::ranges::subrange
. It can be constructed from a pair of iterators or more precisely an iterator and a sentinel. begin()
always returns an iterator, but the type returned by end()
(the "sentinel") does not need to be of the same type as the iterator – as long as they are comparable. This view then holds exactly the iterator-sentinel-pair as its state and nothing else.
But std::ranges::subrange
does not yet facilitate any "composing-behaviour" that you have seen in Ranges tutorial, std::ranges::subrange
is simply a type that can be constructed from an iterator-sentinel-pair, you cannot "pipe" anything into it. Most views are adaptors on other views, e.g. std::ranges::transform_view
wraps an existing view and applies an element-wise transformation on-demand. You can directly construct std::ranges::transform_view
from another view or from non-view ranges that can be wrapped into a view (e.g. references to containers):
std::vector vec{1, 2, 3, 4, 5};
auto l = [] (int i) { return i + 1; };
std::ranges::transform_view v{vec, l};
But this syntax gets difficult to read when you create "a view(from a view(from a view()))". That's why for every view that adapts an existing view we have an additional adaptor object, usually available in a views::
sub-namespace:
std::vector vec{1, 2, 3, 4, 5};
auto l = [] (int i) { return i + 1; };
decltype(detail::transform< trait_t >(list_t{})) transform
Apply a transformation trait to every type in the list and return a seqan3::type_list of the results.
Definition: traits.hpp:471
This adaptor object (std::views::transform
) provides the pipe operator and returns an object of the actual view type (std::ranges::transform_view
). The pipe operator allows us to chain multiple adaptors similar to the unix command line. We will discuss the details of these adaptor objects in the following sections.
Custom range adaptor objects
Read section 24.7 and 24.7.1 of the C++ standard.
The wording of the standard needs some getting used to, but some important notes for us are:
- You can pipe a viewable range into series of adaptor closure objects and will get back a view (this is also what we did above):
std::vector vec{1, 2, 3, 4, 5};
| std::views::filter([] (int i) { return i % 2 == 0; });
- The adaptor objects support function-style usage, too, although it only reduces readability:
std::vector vec{1, 2, 3, 4, 5};
auto v = std::views::filter(
std::views::transform(vec, [] (
int i) {
return i + 1; }), [] (
int i) {
return i % 2 == 0; });
- You can create a new adaptor closure object from an adaptor object that requires parameters by providing those:
std::vector vec{1, 2, 3, 4, 5};
auto v = vec | a;
- You can create a new adaptor closure object from existing adaptor closure objects via
|
but without providing a range: std::vector vec{1, 2, 3, 4, 5};
| std::views::filter([] (int i) { return i % 2 == 0; });
auto v = vec | a;
Terminology | Description | Example |
"view" | A type that models std::ranges::view. | std::ranges::filter_view<std::subrange<int const *, int const *>> |
"view adaptor object" | Creates a view; can be combined with other adaptors. | std::views::reverse and
std::views::filter and
(std::views::filter([] (int) { return true; })) |
"view adaptor closure object" | A "view adaptor object" that requires no paramaters other than the range. | std::views::reverse and std::views::filter and (std::views::filter([] (int) { return true; })) |
In many cases where you are planning on creating "a new view", it will be sufficient to use the previously mentioned techniques to just create "a new adaptor object" and not having to specify the actual view type yourself.
Let's look at some examples!
Exercise 1: Your first custom adaptor object
In the alphabet module you learned that ranges of alphabets are not implicitly convertible to
char
so you
cannot print a
std::vector<seqan3::dna5>
via
std::cout
¹. You also know that you can call
seqan3::to_char
on every object that models
seqan3::alphabet
which will convert it to
char
or a similar type.
We want to do the following:
#include <iostream>
using seqan3::operator""_dna5;
Meta-header for the Alphabet / Nucleotide submodule .
Adaptations of concepts from the Ranges TS.
int main()
{
std::vector<seqan3::dna5> vec{"ATTAGATTA"_dna5};
auto v = vec | my_convert_to_char_view;
std::cout << v[0] << '\n';
}
Define a range adaptor object using an existing adaptor which applies a concrete transformation (calling seqan3::to_char) on every element.
Hint
You need to need use std::views::transform
and you need to set a fixed transformation function. std::views::transform
takes an object that models std::regular_invocable
, e.g. a lambda function with empty capture []
.
¹ You can print via seqan3::debug_stream
, but let's ignore that for now.
Solution
#include <iostream>
using seqan3::operator""_dna5;
{
});
int main()
{
std::vector<seqan3::dna5> vec{"ATTAGATTA"_dna5};
auto v = vec | my_convert_to_char_view;
std::cout << v[0] << '\n';
}
constexpr auto to_char
Return the char representation of an alphabet object.
Definition: concept.hpp:384
You simply define your adaptor type as auto
and make it behave like std::views::transform
, except that you "hard-code" the lambda function that is applied to each element. Since your adaptor object now takes a range as the only parameter, it is an adaptor closure object.
The object is marked as constexpr
because the adaptor object itself never changes, it only provides operator()
and operator|
that each return a specialisation of std::ranges::transformation_view
.
Exercise 2: Combining two existing adaptor objects
Study the seqan3::nucleotide_alphabet
. It states that you can call seqan3::complement
on all nucleotides which will give you 'A'_dna5
for 'T'_dna5
a.s.o. Think about how you can adapt the previous solution to write a view that transforms ranges of nucleotides into their complement.
BUT, we are also interested in reversing the range which is possible with std::views::reverse
:
using seqan3::operator""_dna5;
Provides seqan3::debug_stream and related types.
int main()
{
std::vector<seqan3::dna5> vec{"ACCAGATTA"_dna5};
auto v = vec | my_reverse_complement;
}
debug_stream_type debug_stream
A global instance of seqan3::debug_stream_type.
Definition: debug_stream.hpp:37
Define a range adaptor object that presents a view of the reverse complement of whatever you pipe into it.
Solution
using seqan3::operator""_dna5;
{
});
int main()
{
std::vector<seqan3::dna5> vec{"ACCAGATTA"_dna5};
auto v = vec | my_reverse_complement;
}
constexpr auto complement
Return the complement of a nucleotide object.
Definition: concept.hpp:104
The adaptor consists of std::views::reverse
combined with std::views::transform
. This time the lambda just performs the call to seqan3::complement
.
A full custom view implementation
Using existing adaptors only works to a certain degree, sometimes you will need to implement a full view. As we have seen above, it is easy to implement a view that presents the complement of a nucleotide range just using existing adaptors. For simplicity, we will still use this as an example and implement a non-generic transform view in the next steps.
A full view implementation typically consists of the following components:
- The view class template that takes the underlying range as a template parameter.
- The class should be derived from
std::ranges::view_interface
which will take care of a lot of boilerplate for us.
- The actual functionality of the view is implemented in its iterator and sentinel.
- The adaptor object which allows for the piping behaviour.
1. The view class template
A good thing to start with is to think about the iterator and the sentinel of your view. The iterator and/or its relation to the sentinel is the way we implement the behaviour that is specific to our view. We will start with implementing the iterator separately and later integrate it into the view.
Iterator
Since we know that in our current usecase we have exactly one element in our view for every element in the underlying range, we don't need to change the relation between iterator and sentinel, i.e. "begin == end" on our view iff "begin == end" on the underlying range. This indicates that we can use the sentinel of the underlying range as-is.
Exercise 3: Your first iterator
Have a peak at the C++ Concepts tutorial again and study the std::forward_iterator
concept thoroughly. You will now have to implement your own forward iterator.
#include <iostream>
#include <vector>
using seqan3::operator""_dna5;
template <std::ranges::forward_range urng_t>
struct my_iterator : std::ranges::iterator_t<urng_t>
{
};
static_assert(std::forward_iterator<my_iterator<std::vector<seqan3::dna5>>>);
int main()
{
std::vector<seqan3::dna5> vec{"GATTACA"_dna5};
using my_it_concrete = my_iterator<std::vector<seqan3::dna5>>;
my_it_concrete it{vec.begin()};
while (it != vec.end())
}
In order to re-use functionality of the underlying range's iterator type you can inherit from it (std::ranges::iterator_t
returns the iterator type).¹ In the end, you should be able to iterate over the underlying range and print elements like you would with the original iterator, i.e. your iterator shall behave exactly as the original in this regard (no transformation, yet).
Some things to keep in mind for the implementation:
- When defining a type template (like above), member types are not inherited implicitly (member functions are).
- Another reason "just inheriting" is not sufficient, is that some inherited functions return objects or references to the base type, not your type (which is required by the
std::forward_iterator
concept).
- If you choose to wrap the underlying iterator instead of inheriting, you will need to define and "forward" a few more member functions, but it's good practice and will help you understand the concept.
- The
static_assert
will just return true
or false
, to get more detailed information on why your type does not (yet) model the concept, implement a constrained function template and pass an object of your type to that template.
- Your iterator needs to be comparable to the sentinel of the underlying range, you can access that type via
std::ranges::sentinel_t<urng_t>
.
¹ In some situations it might be better to wrap the underlying iterator instead of inheriting it, i.e. save a copy of the underlying iterator as a data member of your iterator. A reason could be that you don't want to inherit some members or want to prevent implicit convertibility.
Solution
#include <iostream>
#include <vector>
using seqan3::operator""_dna5;
template <std::ranges::forward_range urng_t>
struct my_iterator : std::ranges::iterator_t<urng_t>
{
using base_t = std::ranges::iterator_t<urng_t>;
using value_type = typename std::iterator_traits<base_t>::value_type;
using pointer = typename std::iterator_traits<base_t>::pointer;
using reference = typename std::iterator_traits<base_t>::reference;
using iterator_category = std::forward_iterator_tag;
my_iterator & operator++()
{
base_t::operator++();
return *this;
}
my_iterator operator++(int)
{
my_iterator cpy{*this};
++(*this);
return cpy;
}
};
static_assert(std::forward_iterator<my_iterator<std::vector<seqan3::dna5>>>);
int main()
{
std::vector<seqan3::dna5> vec{"GATTACA"_dna5};
using my_it_concrete = my_iterator<std::vector<seqan3::dna5>>;
my_it_concrete it{vec.begin()};
while (it != vec.end())
}
The program prints "G A T T A C A ".
Exercise 4: A transforming iterator
In the previous assigment you have created a working – but pointless – iterator. It does not do anything differently from the original.
Your task now is to implement the "complementing" behaviour, i.e.
- your iterator should only work on ranges of
seqan3::nucleotide_alphabet
- when accessing an element through the iterator it should not return the element from the underlying range but instead its complement
Think about which operator is responsible for returning the element and be careful with the return type of that operator as your transformation might make a change necessary.
Solution
- The restriction on the alphabet type is done via a
static_assert
:
"You can only iterate over ranges of nucleotides!");
A concept that indicates whether an alphabet represents nucleotides.
You could have done this via an additional template constraint, too, but static_assert
gives you the opportunity to give a readable message in case of an error.¹
- The operator that needs to call the
seqan3::complement
function is operator*
:
reference operator*() const
{
}
- As previously noted, care needs to be taken with this function's return type:
using reference = value_type;
Here is the full solution:
Hint
#include <iostream>
#include <vector>
using seqan3::operator""_dna5;
template <std::ranges::forward_range urng_t>
struct my_iterator : std::ranges::iterator_t<urng_t>
{
"You can only iterate over ranges of nucleotides!");
using base_t = std::ranges::iterator_t<urng_t>;
using value_type = typename std::iterator_traits<base_t>::value_type;
using pointer = typename std::iterator_traits<base_t>::pointer;
using reference = value_type;
using iterator_category = std::forward_iterator_tag;
my_iterator & operator++()
{
base_t::operator++();
return *this;
}
my_iterator operator++(int)
{
my_iterator cpy{*this};
++(*this);
return cpy;
}
reference operator*() const
{
}
};
static_assert(std::forward_iterator<my_iterator<std::vector<seqan3::dna5>>>);
int main()
{
std::vector<seqan3::dna5> vec{"GATTACA"_dna5};
using my_it_concrete = my_iterator<std::vector<seqan3::dna5>>;
my_it_concrete it{vec.begin()};
while (it != vec.end())
}
The program prints "C T A A T G T "
¹ This is only recommended when you do not want to allow a different specialisation of the template to cover the excluded case.
You now have a working iterator, although it still lacks the capabilities of std::bidirectional_iterator
, std::random_access_iterator
and std::contiguous_iterator
. When designing views, you should always strive to preserve as much of the capabilities of the underlying range as possible.
Which of the mentioned concepts do you think your iterator could be made to implement? Have a look at the respective documentation.
Hint
It could be designed to be a std::random_access_iterator
(and thus also std::bidirectional_iterator
) when the underlying range is, because jumping on your iterator/view can be done in constant time iff it can be done in constant time on the underlying range (you just jump to the n-th element of the underlying range and perform your transformation one that).
However, it can never model std::contiguous_iterator
because that would imply that the elements are adjacent to each other in memory (the elements of our view are created on demand and are not stored in memory).
If you have looked at the std::random_access_iterator
, you will have seen that it is quite a bit of work to implement all the operators, many of whom just need to be overloaded to fix the return type. To make this a little bit easier SeqAn provides seqan3::detail::inherited_iterator_base
, it fixes the issue with the return type via CRTP. A solution to the previous exercise looks like this:
#include <iostream>
#include <vector>
using seqan3::operator""_dna5;
template <std::ranges::forward_range urng_t>
class my_iterator : public seqan3::detail::inherited_iterator_base<my_iterator<urng_t>,
std::ranges::iterator_t<urng_t>>
{
private:
"You can only iterate over ranges of nucleotides!");
using base_t = seqan3::detail::inherited_iterator_base<my_iterator<urng_t>,
std::ranges::iterator_t<urng_t>>;
public:
using typename base_t::value_type;
using typename base_t::difference_type;
using typename base_t::iterator_category;
using reference = value_type;
using pointer = void;
my_iterator() = default;
my_iterator(my_iterator const &) = default;
my_iterator(my_iterator &&) = default;
my_iterator & operator=(my_iterator const &) = default;
my_iterator & operator=(my_iterator &&) = default;
~my_iterator() = default;
my_iterator(base_t it) : base_t{
std::move(it)} {}
reference operator*() const noexcept
{
}
reference operator[](difference_type const n) const noexcept
requires std::random_access_iterator<std::ranges::iterator_t<urng_t>>
{
}
pointer operator->() const noexcept = delete;
};
static_assert(
std::random_access_iterator<my_iterator<
std::vector<
seqan3::dna5>>>);
Provides the seqan3::detail::inherited_iterator_base template.
The main SeqAn3 namespace.
Definition: aligned_sequence_concept.hpp:29
SeqAn specific customisations in the standard namespace.
Definition: affine_cell_proxy.hpp:438
int main()
{
std::vector<seqan3::dna5> vec{"GATTACA"_dna5};
using my_it_concrete = my_iterator<std::vector<seqan3::dna5>>;
my_it_concrete it{vec.begin()};
for (size_t i = 0; i < 7; ++i)
The view class
We now implement the view in several steps:
template <std::ranges::view urng_t>
class my_view : public std::ranges::view_interface<my_view<urng_t>>
{
Like the iterator, the view is derived from a CRTP base class that takes care of defining many members for us, e.g. .size()
, .operator[]
and a few others.
The only data member the class holds is a copy of the underlying range. As you may have noted above, our class only takes underlying ranges that model std::ranges::view. This might seem strange; after all we want to apply the view to a vector of which we know that it is not a view, but we will clear this up later.
public:
using iterator = my_iterator<urng_t>;
using const_iterator = my_iterator<urng_t const>;
The only member types that we define here are the definitions of the iterators which are just the iterator we have defined before. Note that we have const_iterator
in addition to iterator
which const-qualified member functions return, because in a const-context the urange
data member will be const so we cannot return the mutable iterator
from it.
Many ranges like the standard library containers also present the member types of the iterator, i.e. value_type
, reference
a.s.o, but this is not required to model any of the range concepts.
auto begin() noexcept
{
return iterator{std::ranges::begin(urange)};
}
auto begin() const noexcept
{
return const_iterator{std::ranges::begin(urange)};
}
auto cbegin() const noexcept
{
return const_iterator{std::ranges::begin(urange)};
}
These functions are the same member functions you know from std::vector
, they return objects of the previously defined iterator types that are initialised with the begin iterator from the underlying range.
auto end() noexcept
{
return std::ranges::end(urange);
}
auto end() const noexcept
{
return std::ranges::end(urange);
}
auto cend() const noexcept
{
return std::ranges::end(urange);
}
The implementation for end()
is similar except that for our range the sentinel type (the return type of end()
) is the same as of the underlying range, we just pass it through.
For many more complex views you will have to define the sentinel type yourself or derive it from the underlying type in a similar manner to how we derived the iterator type. Often you can use std::default_sentinel_t
as the type for your sentinel and implement the "end-condition" in the iterator's equality comparison operator against that type.
my_view(urng_t urange_) : urange{
std::move(urange_)}
{}
template <std::ranges::viewable_range orng_t>
my_view(orng_t && urange_) : urange{
std::views::all(
std::forward<orng_t>(urange_))}
{}
We have two constructors, one that takes an the underlying type by copy and moves it into the data member (remember that since it is a view, it will not be expensive to copy – if it is copied).
The second constructor is more interesting, it takes a std::ranges::viewable_range
which is defined as being either a std::ranges::view
or a reference to std::ranges::range
that is not a view (e.g. std::vector<char> &
). Since we have a constructor for std::ranges::view
already, this one explicitly handles the second case and goes through std::views::all
which wraps the reference in a thin view-layer. Storing only a view member guarantess that our type itself is also cheap to copy among other things.
Note that both of these constructors seem like generic functions, but they just handle the underlying type or a type that turns into the underlying when wrapped in std::views::all
.
template <std::ranges::viewable_range orng_t>
my_view(orng_t &&) -> my_view<std::views::all_t<orng_t>>;
To easily use the second constructor we need to provide a type deduction guide.
Here is the full solution:
Hint
#include <iostream>
#include <vector>
using seqan3::operator""_dna5;
template <std::ranges::forward_range urng_t>
class my_iterator : public seqan3::detail::inherited_iterator_base<my_iterator<urng_t>,
std::ranges::iterator_t<urng_t>>
{
private:
"You can only iterate over ranges of nucleotides!");
using base_t = seqan3::detail::inherited_iterator_base<my_iterator<urng_t>,
std::ranges::iterator_t<urng_t>>;
public:
using typename base_t::value_type;
using typename base_t::difference_type;
using typename base_t::iterator_category;
using reference = value_type;
using pointer = void;
my_iterator() = default;
my_iterator(my_iterator const &) = default;
my_iterator(my_iterator &&) = default;
my_iterator & operator=(my_iterator const &) = default;
my_iterator & operator=(my_iterator &&) = default;
~my_iterator() = default;
my_iterator(base_t it) : base_t{
std::move(it)} {}
reference operator*() const noexcept
{
}
reference operator[](difference_type const n) const noexcept
requires std::random_access_iterator<std::ranges::iterator_t<urng_t>>
{
}
pointer operator->() const noexcept = delete;
};
static_assert(
std::random_access_iterator<my_iterator<
std::vector<
seqan3::dna5>>>);
template <std::ranges::view urng_t>
class my_view : public std::ranges::view_interface<my_view<urng_t>>
{
public:
using iterator = my_iterator<urng_t>;
using const_iterator = my_iterator<urng_t const>;
my_view(urng_t urange_) : urange{
std::move(urange_)}
{}
template <std::ranges::viewable_range orng_t>
my_view(orng_t && urange_) : urange{
std::views::all(
std::forward<orng_t>(urange_))}
{}
auto begin() noexcept
{
return iterator{std::ranges::begin(urange)};
}
auto begin() const noexcept
{
return const_iterator{std::ranges::begin(urange)};
}
auto cbegin() const noexcept
{
return const_iterator{std::ranges::begin(urange)};
}
auto end() noexcept
{
return std::ranges::end(urange);
}
auto end() const noexcept
{
return std::ranges::end(urange);
}
auto cend() const noexcept
{
return std::ranges::end(urange);
}
template <std::ranges::viewable_range orng_t>
my_view(orng_t &&) -> my_view<std::views::all_t<orng_t>>;
int main()
{
std::vector<seqan3::dna5> vec{"GATTACA"_dna5};
using my_it_concrete = my_iterator<std::vector<seqan3::dna5>>;
my_it_concrete it{vec.begin()};
for (size_t i = 0; i < 7; ++i)
my_view v{vec};
static_assert(std::ranges::random_access_range<decltype(v)>);
The program prints
2. The adaptor object
The adaptor object is a function object also called functor. This means we define a type with the respective operators and then create a global instance of that type which can be used to invoke the actual functionality.
Adaptor type definition
The adaptor has the primary purpose of facilitating the piping behaviour, but it shall also allow for function/constructor-style creation of view objects, therefore it defines two operators:
struct my_view_fn
{
template <std::ranges::input_range urng_t>
auto operator()(urng_t && urange) const
{
return my_view{std::forward<urng_t>(urange)};
}
template <std::ranges::input_range urng_t>
friend auto operator|(urng_t && urange, my_view_fn
const &)
{
return my_view{std::forward<urng_t>(urange)};
}
};
auto operator|(validator1_type &&vali1, validator2_type &&vali2)
Enables the chaining of validators.
Definition: validators.hpp:1096
The first operator is very straight-forward, it simply delegates to the constructor of our view so that views::my(FOO)
is identical to my_view{FOO}
.
The second operator is declared as friend, because the left-hand-side of the operator|
is generic, it's how the range is handled in snippets like auto v = vec | views::my
.
- Note
- This adaptor type does not yet provide the ability to combine multiple adaptors into a new adaptor, it only handles ranges as left-hand-side input to
operator|
.
Our example adaptor type definition is rather simple, but for views/adaptors that take more parameters it gets quite complicated quickly. Therefore SeqAn provides some convenience templates for you:
using my_view_fn = seqan3::detail::adaptor_for_view_without_args<my_view>;
See seqan3::detail::adaptor_base
, seqan3::detail::adaptor_for_view_without_args
and seqan3::detail::adaptor_from_functor
for more details.
Adaptor object definition
The adaptor object is simply an instance of the previously defined type:
namespace views
{
inline constexpr my_view_fn my{};
}
As noted above, we place this object in a views::
sub-namespace by convention. Since the object holds no state, we mark it as constexpr
and since it's a global variable we also mark it as inline
to prevent linkage issues.
Finally we can use our view with pipes and combine it with others:
auto v2 = vec | std::views::reverse | ::views::my;
static_assert(std::ranges::random_access_range<decltype(v2)>);
Here is the full, final solution:
Hint
#include <iostream>
#include <vector>
using seqan3::operator""_dna5;
template <std::ranges::forward_range urng_t>
class my_iterator : public seqan3::detail::inherited_iterator_base<my_iterator<urng_t>,
std::ranges::iterator_t<urng_t>>
{
private:
"You can only iterate over ranges of nucleotides!");
using base_t = seqan3::detail::inherited_iterator_base<my_iterator<urng_t>,
std::ranges::iterator_t<urng_t>>;
public:
using typename base_t::value_type;
using typename base_t::difference_type;
using typename base_t::iterator_category;
using reference = value_type;
using pointer = void;
my_iterator() = default;
my_iterator(my_iterator const &) = default;
my_iterator(my_iterator &&) = default;
my_iterator & operator=(my_iterator const &) = default;
my_iterator & operator=(my_iterator &&) = default;
~my_iterator() = default;
my_iterator(base_t it) : base_t{
std::move(it)} {}
reference operator*() const noexcept
{
}
reference operator[](difference_type const n) const noexcept
requires std::random_access_iterator<std::ranges::iterator_t<urng_t>>
{
}
pointer operator->() const noexcept = delete;
};
static_assert(
std::random_access_iterator<my_iterator<
std::vector<
seqan3::dna5>>>);
template <
std::ranges::view urng_t>
class my_view : public
std::ranges::view_interface<my_view<urng_t>>
{
private:
urng_t urange;
public:
using iterator = my_iterator<urng_t>;
using const_iterator = my_iterator<urng_t const>;
my_view(urng_t urange_) : urange{
std::move(urange_)}
{}
template <std::ranges::viewable_range orng_t>
my_view(orng_t && urange_) : urange{
std::views::all(
std::forward<orng_t>(urange_))}
{}
auto begin() noexcept
{
return iterator{std::ranges::begin(urange)};
}
auto begin() const noexcept
{
return const_iterator{std::ranges::begin(urange)};
}
auto cbegin() const noexcept
{
return const_iterator{std::ranges::begin(urange)};
}
auto end() noexcept
{
return std::ranges::end(urange);
}
auto end() const noexcept
{
return std::ranges::end(urange);
}
auto cend() const noexcept
{
return std::ranges::end(urange);
}
};
template <std::ranges::viewable_range orng_t>
my_view(orng_t &&) -> my_view<std::views::all_t<orng_t>>;
struct my_view_fn
{
template <std::ranges::input_range urng_t>
auto operator()(urng_t && urange) const
{
return my_view{std::forward<urng_t>(urange)};
}
template <std::ranges::input_range urng_t>
friend auto operator|(urng_t && urange, my_view_fn
const &)
{
return my_view{std::forward<urng_t>(urange)};
}
};
namespace views
{
inline constexpr my_view_fn my{};
}
int main()
{
std::vector<seqan3::dna5> vec{"GATTACA"_dna5};
using my_it_concrete = my_iterator<std::vector<seqan3::dna5>>;
my_it_concrete it{vec.begin()};
for (size_t i = 0; i < 7; ++i)
my_view v{vec};
static_assert(std::ranges::random_access_range<decltype(v)>);
auto v2 = vec | std::views::reverse | ::views::my;
static_assert(std::ranges::random_access_range<decltype(v2)>);
}
The program prints:
C T A A T G T
CTAATGT
TGTAATC