Should every fancy pointer be an iterator?
Asked Answered
T

1

4

I'm working on a segment-based memory allocator for C++. In this allocator, when you deallocate a chunk of memory, you have to know which segment it came from. Therefore, I'm storing a pointer to the segment as a member of the fancy pointer returned from the allocator's allocate function.

Just to show the interface I'm talking about: here's the fancy_memory_resource that backs my allocator...

template<class Ptr>
class fancy_memory_resource {
public:
    Ptr allocate(size_t bytes, size_t align = alignof(max_align_t)) {
        return do_allocate(bytes, align);
    }
    void deallocate(Ptr p, size_t bytes, size_t align = alignof(max_align_t)) {
        return do_deallocate(p, bytes, align);
    }
    bool is_equal(const fancy_memory_resource& rhs) const noexcept {
        return do_is_equal(rhs);
    }
    virtual ~fancy_memory_resource() = default;
private:
    virtual Ptr do_allocate(size_t bytes, size_t align) = 0;
    virtual void do_deallocate(Ptr p, size_t bytes, size_t align) = 0;
    virtual bool do_is_equal(const fancy_memory_resource& rhs) const noexcept = 0;
};

(Notice that std::pmr::memory_resource can be implemented as a typedef for fancy_memory_resource<void*>. This is intentional on my part.)

Meanwhile, the Ptr in question is a fancy pointer type named segmented_fancy_pointer<T> (not pictured) that inherits from the CRTP type fancy_ptr_base<T, segmented_fancy_pointer<T>>...

template<class T, class CRTP>
struct fancy_ptr_base {
    constexpr T *ptr() const noexcept { return m_ptr; }
    constexpr explicit operator T*() const noexcept { return ptr(); }
    constexpr explicit operator bool() const noexcept { return ptr() != nullptr; }
    constexpr bool operator==(CRTP b) const { return ptr() == b.ptr(); }
    constexpr bool operator!=(CRTP b) const { return ptr() != b.ptr(); }
    constexpr bool operator==(decltype(nullptr)) const { return ptr() == nullptr; }
    constexpr bool operator!=(decltype(nullptr)) const { return ptr() != nullptr; }
    constexpr bool operator<(CRTP b) const { return ptr() < b.ptr(); }
    constexpr bool operator<=(CRTP b) const { return ptr() <= b.ptr(); }
    constexpr bool operator>(CRTP b) const { return ptr() > b.ptr(); }
    constexpr bool operator>=(CRTP b) const { return ptr() >= b.ptr(); }
    constexpr T& operator*() const noexcept { return *ptr(); }
    constexpr T* operator->() const noexcept { return ptr(); }
    constexpr CRTP& operator+=(ptrdiff_t i) { m_ptr += i; return as_crtp(); }
    constexpr CRTP& operator-=(ptrdiff_t i) { m_ptr -= i; return as_crtp(); }
    constexpr CRTP& operator++() { ++m_ptr; return as_crtp(); }
    constexpr CRTP& operator--() { --m_ptr; return as_crtp(); }
    constexpr CRTP operator++(int) { auto r(as_crtp()); ++*this; return r; }
    constexpr CRTP operator--(int) { auto r(as_crtp()); --*this; return r; }
    constexpr CRTP operator+(ptrdiff_t i) const { auto r(as_crtp()); r += i; return r; }
    constexpr CRTP operator-(ptrdiff_t i) const { auto r(as_crtp()); r -= i; return r; }
    constexpr ptrdiff_t operator-(CRTP b) const { return ptr() - b.ptr(); }

protected:
    T *m_ptr = nullptr;
private:
    constexpr CRTP& as_crtp() { return *static_cast<CRTP*>(this); }
    constexpr const CRTP& as_crtp() const { return *static_cast<const CRTP*>(this); }
};

template<class CRTP>
struct fancy_ptr_base<void, CRTP> {
    constexpr void *ptr() const noexcept { return m_ptr; }
    constexpr explicit operator void*() const noexcept { return ptr(); }
    constexpr explicit operator bool() const noexcept { return ptr() != nullptr; }
    constexpr bool operator==(CRTP b) const { return ptr() == b.ptr(); }
    constexpr bool operator!=(CRTP b) const { return ptr() != b.ptr(); }
    constexpr bool operator==(decltype(nullptr)) const { return ptr() == nullptr; }
    constexpr bool operator!=(decltype(nullptr)) const { return ptr() != nullptr; }
    constexpr bool operator<(CRTP b) const { return ptr() < b.ptr(); }
    constexpr bool operator<=(CRTP b) const { return ptr() <= b.ptr(); }
    constexpr bool operator>(CRTP b) const { return ptr() > b.ptr(); }
    constexpr bool operator>=(CRTP b) const { return ptr() >= b.ptr(); }
protected:
    void *m_ptr = nullptr;
};

Now for the real question. When I go to use my segmented_allocator<T> (not pictured) with libc++'s std::vector, it all works fine. When I try to use it with libstdc++'s std::vector, it fails:

In file included from /opt/wandbox/gcc-head/include/c++/8.0.0/bits/stl_algobase.h:67:0,
                 from /opt/wandbox/gcc-head/include/c++/8.0.0/vector:60,
                 from prog.cc:1984:
/opt/wandbox/gcc-head/include/c++/8.0.0/bits/stl_iterator.h: In instantiation of 'class __gnu_cxx::__normal_iterator<scratch::segmented_fancy_pointer<int>, std::vector<int, scratch::pmr::propagating_polymorphic_allocator<int, scratch::segmented_fancy_pointer<int> > > >':
/opt/wandbox/gcc-head/include/c++/8.0.0/bits/vector.tcc:105:25:   required from 'std::vector<_Tp, _Alloc>::reference std::vector<_Tp, _Alloc>::emplace_back(_Args&& ...) [with _Args = {int}; _Tp = int; _Alloc = scratch::pmr::propagating_polymorphic_allocator<int, scratch::segmented_fancy_pointer<int> >; std::vector<_Tp, _Alloc>::reference = int&]'
/opt/wandbox/gcc-head/include/c++/8.0.0/bits/stl_vector.h:954:21:   required from 'void std::vector<_Tp, _Alloc>::push_back(std::vector<_Tp, _Alloc>::value_type&&) [with _Tp = int; _Alloc = scratch::pmr::propagating_polymorphic_allocator<int, scratch::segmented_fancy_pointer<int> >; std::vector<_Tp, _Alloc>::value_type = int]'
prog.cc:1990:18:   required from here
/opt/wandbox/gcc-head/include/c++/8.0.0/bits/stl_iterator.h:770:57: error: no type named 'iterator_category' in 'struct std::iterator_traits<scratch::segmented_fancy_pointer<int> >'
       typedef typename __traits_type::iterator_category iterator_category;
                                                         ^~~~~~~~~~~~~~~~~

Now, I can fix this by adding the "iterator traits" typedefs into fancy_ptr_base<T, CRTP>, like this:

    using pointer = CRTP;
    using reference = T&;
    using value_type = std::remove_cv_t<T>;
    using iterator_category = std::random_access_iterator_tag;
    using difference_type = ptrdiff_t;

But should I have to? Is it required that every fancy pointer type be an iterator type as well? Or is libc++ doing the right thing and libstdc++'s vector just has a bug?

(I have already convinced myself that most iterators are not fancy pointers. This question is motivated by my sudden doubt that perhaps all fancy pointers are indeed iterators.)

Tawnytawnya answered 16/7, 2017 at 19:28 Comment(0)
G
3

Yes, you are required to implement all requirements of a Random Access Iterator. C++ Standard [allocator.requirements]/5:

An allocator type X shall.... X::pointer and X::const_pointer shall also satisfy the requirements for a random access iterator.

So in particular, your fancy pointer type needs the five member types required of every iterator.

You also seem to be missing fancy_memory_resource<Ptr>::value_type, several needed non-member functions, and a number of noexcept keywords. Please review the requirements for allocator types and their pointer types carefully.

Gendarmerie answered 16/7, 2017 at 20:4 Comment(3)
Thanks. Re your last paragraph, nit: I didn't post the code for my segmented_allocator<T>, which is the thing with the value_type. My fancy_memory_resource is just a fancified version of std::pmr::memory_resource, and as such does not have any value_type (it deals in raw bytes, not Ts). What non-member functions were you thinking of, besides == and != and maybe swap?Tawnytawnya
Yeah, == and != for the allocator type, plus supporting nullptr == fancy_ptr, nullptr != fancy_ptr, number + fancy_ptr.Gendarmerie
Ah. I do have == and != for the allocator type (not pictured). As for the Yoda-style == != +, I deliberately left them out of my repo on the YAGNI principle. (I agree they are required by the Standard, though.)Tawnytawnya

© 2022 - 2024 — McMap. All rights reserved.