Skip to content

HowTo write a View add constant_full

Hannes Hauswedell edited this page Feb 13, 2018 · 18 revisions

view_add_constant

We start with the first part of the implementation:

#include <range/v3/all.hpp>
#include <iostream>

template <typename irng_t>
    requires /*input_range_concept<irng_t> &&*/
             std::is_same_v<std::decay_t<ranges::range_reference_t<irng_t>>, uint64_t>
class view_add_constant
{
private:
    /* data members == "the state" */
    irng_t const & irange;

    /* the iterator type */
    struct iterator_t : ranges::iterator_t<irng_t const>
    {
        using base = ranges::iterator_t<irng_t const>;

        iterator_t(base const & b) : base{b} {}

        uint64_t operator*() const
        {
            return *static_cast<base>(*this) + 42;
        }
    };
  • For convenience we have included all of range-v3; in production code, you will want to actually select your required headers
  • view_add_constant is a class template, because it needs to hold a reference to the original range it operates on; this range's type is passed in a as template parameter on which we enforce certain constraints. The most basic constraint is to enforce that it actually is an input range (we have commented this out for demonstration purposed, because the concept is not yet available in ranges – it is available in SeqAn, though, so if you write code in SeqAn, do check this!). The second constraint is that the input range is actually a range over uint64_t (possibly with reference or const).
  • It is important to remember that we always deal with the range_reference_t (not the range_value_t) as dereferencing an iterator or calling [] on a range returns something of the range_reference_t not the range_value_t (there reference type may or may not actually contain a &).
  • Please note that these constraints are specific to the view we are just creating. Other views will have different requirements on the reference type or even the range itself (e.g. it could be required to be a random_access_sequence_concept).
  • The only data member we have is the reference to original range. Since we know that we do not want to change the original values, i.e. we have a read-only view, we save a irng_t const & irange. Other views may very well want to expose the underlying range to modification, you would then have irng_t && irange member.
  • Next we define an iterator type. Since view_add_constant needs to satisfy basic range requirements, you need to be able to iterate over it. In our case we can stay close to the original and inherit from the original iterator (plus const because we hold a const & to the original, see above). We are only overloading one operation: the dereference operation, i.e. actually getting the value. This is the place where we interject and call the base class's dereference, but then add the constant 42. Note that this changes the return type of the operation (reference_t); it used to be uint64_t const & (actually uint64_t &, but we added const above), now it's uint64_t β†’ A new value is always generated as the result of adding 42.
  • Note that more complex views might require drastically more complex iterators and it might make sense to define those externally.

We continue with the public interface:

public:
    /* member type definitions */
    using reference         = uint64_t;
    using const_reference   = uint64_t;
    using value_type        = uint64_t;

    using iterator          = iterator_t;
    using const_iterator    = iterator_t;
  • First we define the member types that are required for input ranges. Of course our value type is uint64_t as we only operate on ranges over uint64_t and we are just adding a number. As we mentioned above, our iterator will always generate new values when dereferenced so the reference types also value types.
  • Note: Other view implementation might be agnostic of the actual value type, e.g. a view that just reverses the elements can do so independent of the type. In that case you pass the reference type through as using reference = range_reference_t<irng_t>;. The value type would then be the reference type with any references stripped (using value_type = std::remove_cv_t<std::remove_reference_t<reference>>; and the const_reference type would be the reference type with const added (except if the reference type already is only a value_type in which case it is also just the value type, like in our above example)ΒΉ.
  • The iterator type is just the type we defined above. Note that in views the iterator and const_iterator are always the same type. So for a view "foo" that can potentially modify the underlying range, a const version of "foo" does not protect the underlying range from modification! Instead create the view over a const version of the underlying range or use a wrapper like ranges::view::const_ that changes the reference type in the pipe to be const and thereby prevents modification.
    /* constructors and deconstructors */
    view_add_constant() = default;
    constexpr view_add_constant(view_add_constant const & rhs) = default;
    constexpr view_add_constant(view_add_constant && rhs) = default;
    constexpr view_add_constant & operator=(view_add_constant const & rhs) = default;
    constexpr view_add_constant & operator=(view_add_constant && rhs) = default;
    ~view_add_constant() = default;

    view_add_constant(irng_t const & _irange)
        : irange{_irange}
    {}
  • The constructors are pretty much standard. We have an extra constructor that initialises our range reference from the value passed in. In C++17 this also serves to determine the template parameter if not specified.
    /* begin and end */
    iterator begin() const
    {
        return std::cbegin(irange);
    }
    iterator cbegin() const
    {
        return begin();
    }

    iterator end() const
    {
        return std::cend(irange);
    }

    iterator cend() const
    {
        return end();
    }
};
  • Finally we add begin and end iterators. Our iterator type can be created from the underlying iterator type, because we added a constructor above. And, as noted above, the const and non-const versions are the same.
  • Note that if you want your view to be stronger that an input_range, e.g. be a sized_range or even a random_access_range, you would need to define additional member types (size_type, difference_type) and additional member functions (size(), operator[]...).
  • To have your new view actually be treated as such by the range-v3-library, you need to also overload this metafunction:
namespace ranges::v3
{

template <typename irng_t>
struct enable_view<view_add_constant<irng_t>> : std::true_type
{};

} // namespace ranges::v3

ΒΉ As new values (rvalues) are returned adding const makes no sense, in fact compilers warn if you do.

add_constant_fn

Off to our second type definition:

struct add_constant_fn
{
    template <typename irng_t>
        requires /*input_range_concept<irng_t> &&*/
                std::is_same_v<std::decay_t<ranges::range_reference_t<irng_t>>, uint64_t>
    auto operator()(irng_t && irange) const
    {
        return view_add_constant{std::forward<irng_t>(irange)};
    }

    template <typename irng_t>
        requires /*input_range_concept<irng_t> &&*/
                std::is_same_v<std::decay_t<ranges::range_reference_t<irng_t>>, uint64_t>
    friend auto operator|(irng_t && irange, add_constant_fn const &)
    {
        return view_add_constant{std::forward<irng_t>(irange)};
    }

};
  • The first operator facilitates something similar to the constructor, it enables traditional usage of the view a la auto v = view::add_constant(other_range);.
  • The second operator enables the pipe notation: auto v = other_range | view::add_constant;. It needs to be friend or a free function and takes two arguments (both sides of the operation).

view::add_constant

Finally we add an instance of add_constant_fn to namespace view:

namespace view
{
    add_constant_fn const add_constant;
}

If you prepend all of the above to the test on HowTo write a View it should work.

Clone this wiki locally