-
Notifications
You must be signed in to change notification settings - Fork 82
HowTo write a View add constant_full
Hannes Hauswedell edited this page Mar 2, 2018
·
18 revisions
We start with the first part of the implementation:
#include <range/v3/all.hpp>
#include <iostream>
template <typename urng_t>
// requires (bool)ranges::InputRange<urng_t>() &&
// std::is_same_v<std::decay_t<ranges::range_reference_t<urng_t>>, uint64_t>
class view_add_constant : public ranges::view_base
{
static_assert(!std::is_same_v<urng_t, std::remove_reference_t<urng_t>>,
"The view must retain either rvalue or lvalue reference to the underlying range.");
- 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 clang, but it works in GCC). The second constraint is that the input range is actually a range overuint64_t
(possibly with reference orconst
). -
It is important to remember that we always deal with the
range_reference_t
(not therange_value_t
) as dereferencing an iterator or calling[]
on a range returns something of therange_reference_t
not therange_value_t
(the 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 satisfy
RandomAccessRange
). - We inherit from
view_base
which is an empty base class, because being derived from it signals to some library checks that this class is a really trying to be a view. - There is another hard constraint, and that is that we create the view over a reference to another range. This can be
&
,const &
or&&
. One of these has be "included" in theurng_t
type. [With GCC this can also be enforced through a concept, but the assert is more verbose and the error is readable.]
private:
/* data members == "the state" */
struct data_members_t
{
urng_t urange;
};
std::shared_ptr<data_members_t> data_members;
- The only data member we have is the reference to original range. It may look like we are saving a value here, but we are not since we previously enforced that
urng_t
contains a reference. Why don't we just saveurng_t const & urange;
? Well, some views are not "read-only" they provide access to the underlying range so we generally don't want to enforceconst
. Another problem is that some ranges might not beconst
-iterable so we don't want to enforceconst
. But a regular lvalue-reference (&
) is not satisfactory either, because we also want to be able to bind temporaries. This is especially necessary in a chain of views inside a pipe where every view functor returns a new temporary. The above solution gives us the possibility to have all kinds of references and automatically pick the least constrained one (more on this below). - Why do we put the member variables inside an extra data structure stored in a smart pointer? A requirement of views is that they be copy-able in constant time, e.g. there should be no expensive operations like allocations during copying. An easy and good way to achieve implicit sharing of the data members is to put them inside a
shared_ptr
. Thereby all copies share the data_members and they get deleted with the last copy. - Remember that our reference type could be
&&
and that makes the enclosing type not copy-able by default, another reason to put it in the shared member! Other more complex views have more variables or "state" that they might be saving in this data structure. - Another plus is that it enables our view to be default-constructible. This is another requirement of views β and having a top-level reference member prevents this. [Of course you can use a top-level pointer instead of a reference, but we don't like raw pointers anymore!]
/* the iterator type */
struct iterator_t : ranges::iterator_t<std::remove_reference_t<urng_t> const>
{
using base = ranges::iterator_t<std::remove_reference_t<urng_t> const>;
iterator_t() = default;
iterator_t(base const & b) : base{b} {}
iterator_t operator++(int)
{
return static_cast<base&>(*this)++;
}
iterator_t & operator++()
{
++static_cast<base&>(*this);
return (*this);
}
uint64_t operator*() const
{
return *static_cast<base>(*this) + 42;
}
};
- 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 (plusconst
because we know we won't be changing it). Forranges::iterator_t<>
to work we need to remove the reference from our type. - For the iterator to satisfy the InputIterator concept we need to overload the increment operators so that their return type is of our class and not the base class. The actually important overload is of 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 beuint64_t const &
(actuallyuint64_t &
, but we addedconst
above), now it'suint64_t
β A new value is always generated as the result of adding42
. - Note that more complex views might require drastically more complex iterators and it might make sense to define those externally. In general iterators involve a lot of boilerplate code, depending on the scope of your project it might make sense to add your own iterator base classes, using CRTP also helps re-use code and not having to create "non-functional" overloads.
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 overuint64_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 are 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<std::remove_reference_t<urng_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 theconst_reference
type would be the reference type withconst
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
andconst_iterator
are always the same type. So for a view "foo" that can potentially modify the underlying range, aconst
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 likeranges::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(urng_t urange)
: data_members{new data_members_t{std::forward<urng_t>(urange)}}
{}
- The constructors are pretty much standard. We have an extra constructor that initialises our range reference from the value passed in. Note that also here the
urng_t
is of a reference type so there are no copies happening anywhere.
/* begin and end */
iterator begin() const
{
return std::cbegin(data_members->irange);
}
iterator cbegin() const
{
return begin();
}
iterator end() const
{
return std::cend(data_members->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 asized_range
or even arandom_access_range
, you would need to define additional member types (size_type
,difference_type
) and additional member functions (size()
,operator[]
...).
template <typename urng_t>
// requires (bool)ranges::InputRange<urng_t>() &&
// std::is_same_v<std::decay_t<ranges::range_reference_t<urng_t>>, uint64_t>
view_add_constant(urng_t &&) -> view_add_constant<std::add_rvalue_reference_t<urng_t>>;
- We add n user-defined type deduction guide for our view.
- Class template argument deduction enables people to use your class template without having to manually specify the template parameter.
- In C++17 there is automatic deduction, as well, but we need user defined deduction, because we never want to have
urng_t
resolve to a value type. The above guide is sufficient to keep&
as&
,const &
asconst &
, but turn value type into&&
. For more information on this, see the rules for reference collapsing and forwarding references.
static_assert((bool)ranges::InputRange<view_add_constant<std::vector<uint64_t>&>>());
static_assert((bool)ranges::View<view_add_constant<std::vector<uint64_t>&>>());
- Now is a good time to check whether your class satisfies the concepts it needs to meet. We have picked
std::vector<uint64_t>&
as an underlying type, but others would work, too. - If the checks fail, you have done something wrong somewhere. The compilers don't yet tell you why certain concept checks fail (especially when using the range library's hacked concept implementation) so you need to add more basic concept checks and try which ones succeed and which break to get hints on which requirements you are failing. A likely candidate is your iterator not meeting the InputIterator concept.
ΒΉ As new values (rvalues) are returned adding const
makes no sense, in fact compilers warn if you do.
Off to our second type definition:
struct add_constant_fn
{
template <typename urng_t>
// requires (bool)ranges::InputRange<urng_t>() &&
// std::is_same_v<std::decay_t<ranges::range_reference_t<urng_t>>, uint64_t>
auto operator()(urng_t && urange) const
{
return view_add_constant{std::add_rvalue_reference_t<urng_t>(urange)};
}
template <typename urng_t>
// requires (bool)ranges::InputRange<urng_t>() &&
// std::is_same_v<std::decay_t<ranges::range_reference_t<urng_t>>, uint64_t>
friend auto operator|(urng_t && urange, add_constant_fn const &)
{
return view_add_constant{std::add_rvalue_reference_t<urng_t>(urange)};
}
};
- The first operator facilitates something similar to the constructor, it enables traditional usage of the view in the so called function-style:
auto v = view::add_constant(other_range);
. - The second operator enables the pipe notation:
auto v = other_range | view::add_constant;
. It needs to befriend
or a free function and takes two arguments (both sides of the operation). - We also add rvalue references here (instead of forwarding), because we want to prevent the
urng_t
type to deduce to a value type.
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.