0

Is it possible to detect the container type from the iterator type?

For example,

#include<traits>
int main(){
   static_assert(std::is_same<
      container_of<std::vector<double>::iterator>::type, std::vector<double>>{});
   static_assert(std::is_same<
      container_of<std::list<int>::iterator>::type, std::list<int>>{});
}

(Of course some iterators type will not give a container (or not give a unique container), for example a raw pointer or a stream iterator, but in those cases it can soft-SFINAE-fail.)

The first attempt is

template<class T, template<class> class Cont> Cont<T> aux(typename Cont<T>::iterator it);

template<class Iterator> struct container_of{
  using type = decltype(aux(Iterator{}));
};

However, it doesn't work because the compiler can't detect the type of T (it is not in a deductible context).


Motivation: I want to detect whether the associated container of an iterator has a .data() member.

alfC
  • 14,261
  • 4
  • 67
  • 118
  • Quick answer: checking for the existence of a .data() member will be easier to implement than a container_of trait. – DeiDei Mar 23 '17 at 07:24
  • This is totally impossible, ever. An iterator type is just a nested type in some class, or perhaps just a type not nested anywhere. There is no way to recover which class it nests in, if any. – n. m. could be an AI Mar 23 '17 at 07:26
  • You can write a trait for *known* container/iterator types. The motivation is a bit unclear though. `data()` is not useful with iterators, you need a container object to call it. – n. m. could be an AI Mar 23 '17 at 07:31
  • 2
    I think he is trying to infer that the underlying container is contiguous. – Giovanni Funchal Mar 23 '17 at 07:35
  • @n.m. I know. The point is the following. It is practically *possible* to check if a container is contiguos. This is because, in practice the presence of a `.data()` member gives that away. However, this trick cannot be used for an iterator, not even in principle apparently. – alfC Mar 23 '17 at 07:36
  • Relevant: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3884.pdf – Giovanni Funchal Mar 23 '17 at 07:38
  • @GiovanniFunchal, yes, that is what I want. To be more specific I want to know the the iterator itself is contiguous. – alfC Mar 23 '17 at 07:45
  • Ok, so you want to know if you can work with something as a continguous block of memory. Why do you have to start with an iterator? Note that the concept of contiguous containers was added in C++17, but I'm uncertain if detecting it is supported. Please describe your underlying problem, not *just* what is blocking your solution to the underlying problem. – Yakk - Adam Nevraumont Mar 23 '17 at 15:23
  • @Yakk, I am using underlying C-code that works with pointers and I want to wrap that around in an iterator interface. If the iterator is a forward iterator it will fall back to a generic element by element version. If the iterator is contiguous then it can use the C-interface. e.g. `void cfunction(void* first, void* last, size_t elemsize){...}` is given, then have a `template function(It first, It last){...}`. If `It` is detected to be contiguous then `cfunction` can be called directly in the `std::addressof(*first/last) `, otherwise it fallsback to element-wise function call. – alfC Mar 23 '17 at 18:31

3 Answers3

2

Instead of your primitive being an iterator, make your primitive be a range.

template<class It, bool Contiguous, class D=void>
struct range_t {
  using Self = std::conditional< !std::is_same<D, void>, D, range_t >;
  It b, e;
  It begin() const { return b; }
  It end() const { return e; }
  Self without_front( std::size_t i = 1 ) const {
    return {std::next(begin(), i), end()};
  }
  Self without_back( std::size_t i = 1 ) const {
    return {begin(), std::prev(end(), i)};
  }
  bool empty() const { return begin()==end(); }
  std::size_t size() const { return std::distance( begin(), end() ); }
};
template<class It>
struct range_t<It, true, void>:
  range_t<It, false, range_t<It, true>>
{
  using Base = range_t<It, false, range_t<It, true>>;
  range_t( It b, It e ):Base(b,e) {}
  auto* data() const {
    if (empty()) return nullptr;
    return std::addressof(*this->begin()); }
  }
};

Track (manually) what containers are contiguous:

template<class T, class=void>
struct is_contiguous_container : std::false_type{};
template<class T>
struct is_contiguous_container<T const, void> : is_contiguous_container<T> {};
template<class T>
struct is_contiguous_container<T volatile, void> : is_contiguous_container<T> {};
template<class T>
struct is_contiguous_container<T const volatile, void> : is_contiguous_container<T> {};
template<class T>
struct is_contiguous_container<T, std::enable_if_t< has_data_ptr<T>{} >>:
  std::true_type{};
template<class T, std::size_t N>
struct is_contiguous_container<T[N],void> : std::true_type{};

The contiguous containers are array, std::array and std::vector, so not much to track. range_t< ?, true, ? > is also contiguous. Just write has_data_ptr, that is true iff T.data() returns a pointer to non-void.

template<class C>
auto range( C&& c ) {
  using std:begin; using std::end;
  auto b = begin(c), e = end(c);
  using It = decltype(b);
  using R = range_t<It, is_contiguous_container<std::remove_reference_t<C>>{}>;
  return R{ b, e };
}

range now smartly converts a container into a range_t, keeping track of if it is contiguous or not.

range_t supports r.without_front( r.size()/2 ) to divide and conquer.

When a range is contiguous, just call .data() on it. When it isn't, don't.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • This is more or less without what I figure out, but instead of tracking containers manually, I was tracking iterators manually (which require a more complicated specialization). But the problem is the same, there is a very large number of iterators `std::vector` `std::array` `boost::array` `boost::static_vector` `boost::flat_set` ... . – alfC Mar 23 '17 at 19:17
  • @alfC A test for `container.data()` returning a pointer that matches `std::decay_t` in type catches most of that list. – Yakk - Adam Nevraumont Mar 23 '17 at 19:21
  • Yes, it is a bit circular. I think if I make an interface that uses containers (or some version of ranges) then (by a design accident) one can detect contiguity from `.data()`. Sadly, I cannot do the same if I want to base the interface on iterators. Perhaps you are right to suggest to base the interface in containers/ranges rather than pointers. – alfC Mar 23 '17 at 20:14
  • @alfC Using ranges (containers are a kind of range) covers a whole multitude of sins. The C++ standard committee is even coming around, with Rangev3 being a range-based set of algorithms and operations. – Yakk - Adam Nevraumont Mar 23 '17 at 20:30
0

In your application if you just want to know whether the container has a .data() member, it might be enough for you to check if it is random access (using std::iterator_traits<Iter>::iterator_category()).

Otherwise, I think you might be able to use a combination of the technique in: How to check if two types come from the same templated class and partial specialization for every standard container type.

Or wait for c++17 which has a new contiguous iterator concept: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4284.html

Community
  • 1
  • 1
Giovanni Funchal
  • 8,934
  • 13
  • 61
  • 110
  • Thanks. (1) `.data()` implies more than `RandomAccess`. Some RandomAccess iterators correspond to a container without the `.data()` member (`std::queue`) (2) Also, with the link I can detect if an iterator is an `std::vector::iterator` with some `T`, but I can't detect if it is `Container::iterator` with some `Container`. (3) The contiguous iterator concept is a half baked thing at the moment. See here http://stackoverflow.com/questions/42851957/contiguous-iterator-detection – alfC Mar 23 '17 at 07:43
  • It would be nice if contiguous iterators either have a `.data()` member or `data(...)` function or are convertible to pointers. That would give away programatically that they are contiguous. – alfC Mar 23 '17 at 07:46
0

What I am doing at the moment is to manually register all (some really) of the iterators that are contiguous.

Since I will always need this in combination with some way to extract the raw pointer, I directly code a single function called data that returns the pointer.

The code is not funny, it considers std::vector<>::iterator, std::basric_string<>::iterator, for illustration (to show that it will always be incomplete) I also added boost::static_vector<>, raw pointer and anything convertible to a pointer. (boost::array<>::iterator and std::array<>::iterator and begin/end(std::valarray) are effectively included because the iterators are pointers). I also had to include the const_iterator cases.

#include<type_traits>
#include<vector> // the code below needs to know about std::vector
#include<boost/container/static_vector.hpp> // ... and all possible contigous containers :(

template<
    class ContiguousIterator, // well ProbablyContiguos
    typename = std::enable_if_t<

        /**/std::is_same<ContiguousIterator, typename std::vector<std::decay_t<decltype(*std::declval<ContiguousIterator>())>>::iterator>{}
        or  std::is_same<ContiguousIterator, typename std::vector<std::decay_t<decltype(*std::declval<ContiguousIterator>())>>::const_iterator>{}

        or  std::is_same<ContiguousIterator, typename std::basic_string<std::decay_t<decltype(*std::declval<ContiguousIterator>())>>::iterator>{}

        or  std::is_same<ContiguousIterator, typename boost::container::static_vector<std::decay_t<decltype(*std::declval<ContiguousIterator>())>, 1>::iterator>{}
        or  std::is_same<ContiguousIterator, typename boost::container::static_vector<std::decay_t<decltype(*std::declval<ContiguousIterator>())>, 1>::const_iterator>{}
        // many many other possible iterators :(

        or  std::is_pointer<ContiguousIterator>{}
        or  std::is_constructible<typename std::iterator_traits<ContiguousIterator>::pointer, ContiguousIterator>{}
    >
>
typename std::iterator_traits<ContiguousIterator>::pointer
data(ContiguousIterator const& it){return std::addressof(*it);}

int main(){
    std::vector<double> v(30);
    v[0] = 10.;
    assert( *data(v.begin()) == 10. );
}

Feedback is welcomed.

alfC
  • 14,261
  • 4
  • 67
  • 118