I'm making a simple, non-owning array view class:
template <typename T>
class array_view {
T* data_;
size_t len_;
// ...
};
I want to construct it from any container that has data()
and size()
member functions, but SFINAE-d correctly such that array_view
is only constructible from some container C
if it would then be valid and safe behavior to actually traverse data_
.
I went with:
template <typename C,
typename D = decltype(std::declval<C>().data()),
typename = std::enable_if_t<
std::is_convertible<D, T*>::value &&
std::is_same<std::remove_cv_t<T>,
std::remove_cv_t<std::remove_pointer_t<D>>>::value>
>
array_view(C&& container)
: data_(container.data()), len_(container.size())
{ }
That seems wholly unsatisfying and I'm not even sure it's correct. Am I correctly including all the right containers and excluding all the wrong ones? Is there an easier way to write this requirement?
If we take a look at the proposed std::experimental::array_view
in N4512, we find the following Viewable
requirement in Table 104:
Expression Return type Operational semantics v.size() Convertible to ptrdiff_t v.data() Type T* such that T* is static_cast(v.data()) points to a implicitly convertible to U*, contiguous sequence of at least and is_same_v<remove_cv_t<T>, v.size() objects of (possibly remove_cv_t<U>> is true. cv-qualified) type remove_cv_t<U>.
That is, the authors are using essentially the same check for .data()
, but add another one for .size()
.
In order to use pointer arithmetic on U
by using operations with T
, the types need to be similar according to [expr.add]p6. Similarity is defined for qualification conversions, this is why checking for implicit convertibility and then checking similarity (via the is_same
) is sufficient for pointer arithmetic.
Of course, there's no guarantee for the operational semantics.
In the Standard Library, the only contiguous containers are std::array
and std::vector
. There's also std::basic_string
which has a .data()
member, but std::initializer_list
does not, despite it being contiguous.
All of the .data()
member functions are specified for each individual class, but they all return an actual pointer (no iterator, no proxy).
This means that checking for the existence of .data()
is currently sufficient for Standard Library containers; you'd want to add a check for convertibility to make array_view
less greedy (e.g. array_view<int>
rejecting some char* data()
).
The implementation can of course be moved away from the interface; you could use Concepts, a concepts emulation, or simply enable_if
with an appropriate type function. E.g.
template<typename T, typename As,
typename size_rt = decltype(std::declval<T>().size())
typename data_rt = decltype(std::declval<T>().data())>
constexpr bool is_viewable =
std::is_convertible_v<size_rt, std::ptrdiff_t>
&& std::is_convertible_v<data_rt, T*>
&& std::is_same_v<std::remove_cv_t<T>, std::remove_cv_t<data_rt>>;
template <typename C,
typename = std::enable_if_t<is_viewable<C, T>>
>
array_view(C&& container)
: data_(container.data()), len_(container.size())
{ }
And yes, that doesn't follow the usual technique for a type function, but it is shorter and you get the idea.