Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Memory allocator with custom pointer type

I tried to create a custom memory allocator which uses a smart pointer. I do not post the code because it's too big and doesn't add much of information. Then I tested it with a std::vector. It works perfectly well on Xcode. But when I tried to build the same code in Visual Studio 12 (2013), the build failed with the following error:

...vector(873): error C2660: 'std::_Wrap_alloc< my_allocator< int > >::construct' : function does not take 2 arguments

the problem is in push_back method:

void push_back(value_type&& _Val)
    {
    ....
        this->_Getal().construct(this->_Mylast,
            _STD forward<value_type>(this->_Myfirst[_Idx]));
    ....
    }

The error message is a bit confusing. Real problem is that this->_Mylast is of type my_allocator< int >::pointer, which is a smart pointer, and construct method expects int*.

So, the question is simple: what are the requirements to pointer types used in a custom memory allocator? Should X::pointer be convertible to a raw pointer? If yes, it makes them pretty useless.

Actually I would expect that line of code to look like:

this->_Getal().construct(addressof(*(this->_Mylast)),
            _STD forward<value_type>(this->_Myfirst[_Idx]));

Let's try to find an answer in C++ standard, which says:

[17.6.3.5-5] An allocator type X shall satisfy the requirements of CopyConstructible (17.6.3.1). The X::pointer, X::const_pointer, X::void_pointer, and X::const_void_pointer types shall satisfy the requirements of NullablePointer (17.6.3.3). No constructor, comparison operator, copy operation, move operation, or swap operation on these types shall exit via an exception. X::pointer and X::const_pointer shall also satisfy the requirements for a random access iterator (24.2)

If we take a look at NullablePointer reqs, they add few other requirements:

[17.6.3.3] A NullablePointer type is a pointer-like type that supports null values. A type P meets the requirements of NullablePointer if:
(1.1) — P satisfies the requirements of EqualityComparable, DefaultConstructible, CopyConstructible, CopyAssignable, and Destructible...

If I check random access iterator requirements, I also don't find any explicit mentioning of its casting to a raw pointer. But in few places the approach with addressof is used (e. g. 24.2.1-5).

Also, it's not the only place in Microsoft's std::vector implementation where X::pointer and raw pointer are assumed to be equal. I'm wondering, what do I miss?

EDIT: I'll add a piece of my_allocator deffinition here:

class my_allocator
{
public:

typedef std::size_t          size_type;
typedef std::ptrdiff_t       difference_type;
typedef my_ptr<T>            pointer;
typedef my_ptr<const T>      const_pointer;
typedef T&                   reference;
typedef const T&             const_reference;
typedef T                    value_type;
typedef my_ptr<void>         void_pointer;
typedef my_ptr<const void>   const_void_pointer;

<constructors>

pointer allocate(size_type n, const_void_pointer = nullptr);
void deallocate(const pointer& ptr, size_type elements_num);
};
like image 774
MaksymB Avatar asked Dec 25 '14 11:12

MaksymB


People also ask

What is custom memory allocator?

This custom memory allocator is optimized for the allocation of small objects. It maintains chains of blocks that are the same size; the number of chains and maximum size of allocated object can be customized.

What is custom memory?

We use the term custom memory allocation in a proscribed way to denote any memory allocation mechanism that differs from general- purpose allocation in at least one of two ways. First, a custom allo- cator may provide more than one object for every allocated chunk of memory.

What is a linear allocator?

Linear Allocation As the name suggests, memory is allocated linearly. Throughout this series, I will be using the concept of an allocator as a means to allocate this memory. A linear allocator, is also known by other names such as an Arena or Region-based allocator.


1 Answers

To solve this problem I created a to_raw_pointer function which happens to work on any "fancy pointer" which implements operator->(). You can find it in the libc++ implementation.

Here it is:

template <class _Tp>
inline _LIBCPP_INLINE_VISIBILITY
_Tp*
__to_raw_pointer(_Tp* __p) _NOEXCEPT
{
    return __p;
}

template <class _Pointer>
inline _LIBCPP_INLINE_VISIBILITY
typename pointer_traits<_Pointer>::element_type*
__to_raw_pointer(_Pointer __p) _NOEXCEPT
{
    return _VSTD::__to_raw_pointer(__p.operator->());
}

It works by calling the operator->() in an unconventional way. This operator must either call another operator->(), or return a real pointer. The overload for real pointers breaks the recursion with an identity function. So this would be used like:

this->_Getal().construct(__to_raw_pointer(this->_Mylast),
            _STD forward<value_type>(this->_Myfirst[_Idx]));

construct is specified to take a real pointer, not a fancy pointer. And there is no implicit conversion specified from fancy pointers to real pointers. The container must use something such as to_raw_pointer, or addressof.

The container is also required to call construct via allocator_traits, instead of calling it directly on the stored allocator as shown. This is to allow construct to be "defaulted" by allocator_traits, as opposed to requiring the allocator to implement construct.

Currently both operator*() and operator->() generally require the fancy pointer to be non-null prior to calling that operator on it. However I am anticipating that this requirement will be relaxed for operator->() in the future.

Update

I was in a bit of a hurry when I wrote the above. Now that I have the time, I was going to include the complete requirements on the allocator::pointer types. However on re-reading the question I see that Maxym has already done a good job of that in the question, so I won't repeat them here.

The one thing that is in the std, but is not completely obvious, is the implicit and explicit conversions among the four pointer types: pointer, const_pointer, void_pointer, and const_void_pointer:

implicit allocator pointer conversions:
+--------------------------------------+
| pointer      -->  const_pointer      |
|    |    \               |            |
|    |      ---------     |            |
|   \|/             _\|  \|/           |
| void_pointer -->  const_void_pointer |
+--------------------------------------+


explicit allocator pointer conversions:
+--------------------------------------+
| pointer           const_pointer      |
|   /|\                  /|\           |
|    |                    |            |
|    |                    |            |
| void_pointer      const_void_pointer |
+--------------------------------------+

That is, you can implicitly convert from non-const to const, and from non-void to void, and you can explicitly convert from void to non-void. But there is no way for a container to const_cast (cast away const-ness) from an allocator::const_pointer or allocator::const_void_pointer. Once the container goes const, it can never get back.

like image 143
Howard Hinnant Avatar answered Oct 17 '22 10:10

Howard Hinnant