2

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?

Drax
  • 12,682
  • 7
  • 45
  • 85
Barry
  • 286,269
  • 29
  • 621
  • 977
  • @dyp Yeah in retrospect it makes no sense, lemme take it out. – Barry Sep 25 '15 at 22:07
  • So what's the relation to the proposed `array_view`? E.g. [N4512](http://open-std.org/JTC1/SC22/WG21/docs/papers/2015/n4512.html) (not sure if that's the current revision). Do you want us to criticize that paper, or compare your approach to theirs? – dyp Sep 25 '15 at 22:10
  • @dyp Never heard of that paper, so... I dunno. Whatever you want to answer :) – Barry Sep 25 '15 at 22:12
  • @dyp Right, I'm half-broken for `string` (can do `array_view` at least) and fully broken for `initializer_list`. Guess the One True Overload idea won't fly. – Barry Sep 25 '15 at 22:23
  • Well I'd argue that you're not broken for `string`. If some function defines an interface `void f(array_view)`, you shouldn't be able to use a `string`. Not sure why `initializer_list` has no `.data` member. – dyp Sep 25 '15 at 22:24
  • Btw, the Guideline Support Library (Stroustrup & Sutter et al) proposes a mutable `string_view`, so I guess some form of mutable `string::data` will be required as well. – dyp Sep 25 '15 at 22:25

1 Answers1

3

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.

dyp
  • 38,334
  • 13
  • 112
  • 177
  • No sure what you expected, Barry. I would have guessed you've found all this info already. But I don't think there's a significantly shorter non-Concept version of the check. – dyp Sep 25 '15 at 22:49
  • Yeah I guess I was hoping there was something I was missing. I do like the idea of putting the check in a constexpr function. – Barry Sep 26 '15 at 00:04