In my last post I explored the special relationship of views and constness. If you have not read it yet, I suggest doing so in order to better understand this post. For example, it explains that "view" refers to classes that do not own the data they refer to, as opposed to e.g. std::view. This post is about writing versatile custom view classes.
In the last post, some principles have been found:
One might feel that all of this is rubbish. The language allows to create non-const views, so why not do it? After all, why not pass mutable references to views? The language also allows usage of output parameters, which is still considered bad style. The sole reason for passing a mutable reference to a view is to change it, making the parameter an output parameter. Instead, pass by const reference or by value and return the modified view. If the function already returns something else then it is probably doing too much already and needs to be refactored.
The last post outlines that there are four different ways to create views. These differ in constness. Following the rules, only two are allowed to be used. For sake of completeness all of them will be included below, however.
This section is about making sure that the custom view class works in all possible scenarios. There are four ways to create views that differ by constness. Regarding initialization, there are assignment and copy construction. These can again be distinguished based on whether they accept rvalue or lvalue references. There is also initialization from view-defining data, which might be a pointer and a size. Not all of them are allowed by the language, e.g. a view<T> can never be initialized by a view<const T>. This is because the view's internal pointer would lose its constness during construction, which is a compile time error. This is good - when a view<const T> is used, the T is usually const for a reason and it should stay that way.
Result type | Initialization mechanism | Initialization value | Works? |
---|---|---|---|
view<T> | pointer, size... | T* | Yes |
view<T> | pointer, size... | const T* | No |
view<T> | copy | view<T>& | Yes |
view<T> | copy | view<T> const& | Yes |
view<T> | copy | view<const T>& | No |
view<T> | copy | view<const T> const& | No |
view<T> | move | view<T>&& | Yes |
view<T> | move | view<const T>&& | No |
view<T> | copy assignment | view<T>& | Yes |
view<T> | copy assignment | view<T> const& | Yes |
view<T> | copy assignment | view<const T>& | No |
view<T> | copy assignment | view<const T> const& | No |
view<T> | move assignment | view<T>&& | Yes |
view<T> | move assignment | view<const T>&& | No |
view<const T> | pointer, size... | T* | Yes |
view<const T> | pointer, size... | const T* | Yes |
view<const T> | copy | view<T>& | Yes |
view<const T> | copy | view<T> const& | Yes |
view<const T> | copy | view<const T>& | Yes |
view<const T> | copy | view<const T> const& | Yes |
view<const T> | move | view<T>&& | Yes |
view<const T> | move | view<const T>&& | Yes |
view<const T> | copy assignment | view<T>& | Yes |
view<const T> | copy assignment | view<T> const& | Yes |
view<const T> | copy assignment | view<const T>& | Yes |
view<const T> | copy assignment | view<const T> const& | Yes |
view<const T> | move assignment | view<T>&& | Yes |
view<const T> | move assignment | view<const T>&& | Yes |
Note:
The above table is not helpful when it comes to passing views by reference. Again, there are several cases, depending on constness and whether an lvalue or rvalue reference is passed.
Argument type | Mechanism | Passed value | Works? |
---|---|---|---|
view<T>& | reference copy | view<T>& | Yes |
view<T>& | reference copy | view<T> const& | No |
view<T>& | temporary value | view<T>&& | No |
view<T> const& | reference copy | view<T>& | Yes |
view<T> const& | reference copy | view<T> const& | Yes |
view<T> const& | temporary value | view<T>&& | Yes |
view<const T>& | temporary value | view<T>& | No* |
view<const T>& | temporary value | view<T> const& | No |
view<const T>& | temporary value | view<T>&& | No |
view<const T> const& | temporary value | view<T>& | Yes |
view<const T> const& | temporary value | view<T> const& | Yes |
view<const T> const& | temporary value | view<T>&& | Yes |
view<const T>& | reference copy | view<const T>& | Yes |
view<const T>& | reference copy | view<const T> const& | No |
view<const T>& | temporary value | view<const T>&& | No |
view<const T> const& | reference copy | view<const T>& | Yes |
view<const T> const& | reference copy | view<const T> const& | Yes |
view<const T> const& | temporary value | view<const T>&& | Yes |
Note:
In the second table, the cases where initialization of a view<T> from a view<const T> is attempted were omitted right away. All of those also fail in the first table. What also fails is initialization of mutable references from const references. Another reason for failure is initialization of a mutable reference with a temporary. A temporary object is created when accepting an lvalue reference, and an rvalue reference is passed. The language specifies that such a temporary is only acceptable if the reference is const.
This leaves one case in the second table which is marked by an asterisk. Note that this only happens when disregarding the main rule of passing views: i.e. when the view is passed by mutable reference. The function call fails. In order to obtain a view<const T>& from a view<T>&, a temporary view<const T> must be created. As explained above, such a temporary can only be bound to a const reference. This is the only case that can be "solved" using the dirty hack outlined in the forbidden section of the last post. Really, you should never have to use it. It's a sign of bad code and bad architechture.
As a purely temporary fix, until the architecture is fixed, a view<const T> temporary can be created manually and passed to the function. Afterwards, the original view can be adapted using the state of the temporary after the function call. It will not be possible to change the pointer of the original view by assigning from the temporary, however - the temporary has a const T pointer. Now it is possible to use pointer arithmetic to find out how much the pointer of the temporary was changed... Obviously, these are extreme hacks. Avoid them at all costs.
Below, an example of a custom view is shown. Additionally, examples (in order) for each of the cases in both tables are shown.
#include <type_traits>
#include <cstdint>
#include <algorithm>
struct Region {
std::uint8_t u8{};
std::uint16_t u16{};
std::uint32_t u32{};
};
template <typename T>
class view final {
public:
using element_type = T;
using pointer = element_type*;
view() = default;
~view() = default;
constexpr pointer const& data() const noexcept {return m_ptr;}
constexpr pointer& data() noexcept {return m_ptr;}
constexpr Region const& region() const {return m_region; }
constexpr Region& region() {return m_region;}
explicit constexpr view(pointer ptr, Region r) : m_ptr{ptr}, m_region{std::move(r)} {}
template <typename U>
constexpr view(view<U> const& v) : view(v.data(), v.region()) {}
template <typename U>
constexpr view(view<U>&& v) : view(v.data(), v.region()) {}
constexpr view(view const&) = default;
constexpr view(view&&) = default;
constexpr view& operator=(view const&) & = default;
constexpr view& operator=(view&&) & = default;
private:
pointer m_ptr{nullptr};
Region m_region;
};
void accept_int_mutable_ref(view<int> &);
void accept_int_const_ref(view<int> const&);
void accept_const_int_mutable_ref(view<const int>&);
void accept_const_int_const_ref(view<const int> const&);
// Calls are equivalent to initialization by reference or move.
void accept_int_value(view<int>);
void accept_const_int_value(view<const int>);
int main(int, char*[]) {
Region r{1,2,3};
int i{10};
const int j{11};
// input objects
view<int> vi{&i, r};
view<const int> vci{&i, r};
const view<int> cvi{&i, r};
const view<const int> cvci{&i, r};
// pointer, size...
view<int> v1{&i, r};
// view<int> v2{&j, r}; // Initialization from const int
// copy
view<int> v3{vi};
view<int> v4{cvi};
// view<int> v5{vci}; // Initialization from const int
// view<int> v6{cvci}; // Initialization from const int
// move
view<int> v7{std::move(vi)};
// view<int> v8{std::move(vci)}; // Initialization from const int
// copy assignment
view<int> v9 = vi;
view<int> v10 = cvi;
// view<int> v11 = vci; // Initialization from const int
// view<int> v12 = cvci; // Initialization from const int
// move assignment
view<int> v13 = std::move(vi);
// view<int> v15 = std::move(vci); // Initialization from const int
view<const int> w1{&i, r};
view<const int> w2{&j, r};
// copy
view<const int> w3{vi};
view<const int> w4{cvi};
view<const int> w5{vci};
view<const int> w6{cvci};
// move
view<const int> w7{std::move(vi)};
view<const int> w8{std::move(vci)};
// copy assignment
view<const int> w9 = vi;
view<const int> w10 = cvi;
view<const int> w11 = vci;
view<const int> w12 = cvci;
// move assignemnt
view<const int> w13 = std::move(vi);
view<const int> w14 = std::move(vci);
/***** function calls *****/
accept_int_mutable_ref(vi);
// accept_int_mutable_ref(cvi); // Const value for non-const reference
// accept_int_mutable_ref(std::move(vi)); // Cannot bind temporary to non-const reference
accept_int_const_ref(vi);
accept_int_const_ref(cvi);
accept_int_const_ref(std::move(vi));
// accept_const_int_mutable_ref(vi); // Cannot bind temporary to non-const reference
// accept_const_int_mutable_ref(cvi); // Const value for non-const reference
// accept_const_int_mutable_ref(std::move(vi)); // Cannot bind temporary to non-const reference
accept_const_int_const_ref(vi);
accept_const_int_const_ref(cvi);
accept_const_int_const_ref(std::move(vi));
accept_const_int_mutable_ref(vci);
// accept_const_int_mutable_ref(cvci); // Const value for non-const reference
// accept_const_int_mutable_ref(std::move(vci)); // Cannot bind temporary to non-const reference
accept_const_int_const_ref(vci);
accept_const_int_const_ref(cvci);
accept_const_int_const_ref(std::move(vci));
return 0;
}
Note: