Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom pointer types and container/allocator typedefs

Tags:

c++

c++11

C++ standard containers and allocators provide typedefs for the pointer type used by the container, i.e.:

typename std::vector<T>::pointer
typename std::vector<T>::const_pointer

The actual pointer type used to create the typedef is determined via std::allocator_traits

typedef typename std::allocator_traits<Allocator>::pointer pointer;

Since each container also has a value_type typedef, presumably the purpose of the pointer typedef is for some weird situation where the pointer type used is something other than value_type*. I've never personally seen a use case for something like that, but I suppose the standards committee wanted to provide the possibility of using custom pointer types with containers.

The problem is that this seems to be inconsistent with the definitions provided for the functions in std::allocator_traits. Specifically, in std::allocator_traits we have the construct function, which is defined as:

template <class T, class... Args>
static void construct(Alloc& a, T* p, Args&&... args);

...which just calls a.construct(p, std::forward<Args>(args)...)

But note that this function makes no provisions for a custom pointer type. The parameter p is a plain-old native pointer.

So, why isn't the definition of this function something like:

template <class... Args>
static void construct(Alloc& a, typename Alloc::pointer p, Args&&... args);

It seems without this, containers which used std::allocator_traits<Alloc>::construct would fail if used with an Allocator that defines some custom pointer type.

So, what's going on here? Or am I misunderstanding the purpose of having pointer typedefs in the first place?

like image 769
Siler Avatar asked Feb 14 '15 22:02

Siler


1 Answers

This dichotomy is purposeful, and does not present a problem. The construct member function is typically implemented like this:

template <class U, class ...Args>
void
construct(U* p, Args&& ...args)
{
    ::new(static_cast<void*>(p)) U(std::forward<Args>(args)...);
}

I.e. it forwards to placement new, which in turn has this signature:

void* operator new  (std::size_t size, void* ptr) noexcept;

So ultimately you need a "real" pointer to call placement new. And to convey the type of the object that needs to be constructed, it just makes sense to pass that information along in the pointer type (e.g. U*).

For symmetry, destroy is also formulated in terms of an actual pointer and is typically implemented like so:

template <class U>
void
destroy(U* p)
{
    p->~U();
}

The main use case for the "fancy pointer" is to place objects into shared memory. A class called offset_ptr is typically used for this purpose, and an allocator can be created to allocate and deallocate memory referred to by offset_ptr. And thus the allocator_traits and allocator allocate and deallocate functions traffic in terms of pointer instead of value_type*.

So the question arises: If you've got a pointer, and need a T*, what do you do?

There are two techniques I'm aware of for creating a T* from a pointer p

1. std::addressof(*p);

When you dereference a pointer p, it must result in an lvalue according to the standard. However it would be nice to be able to relax this requirement (e.g. consider a pointer returning a proxy reference such as vector<bool>::reference). std::addressof is specified to to return a T* to any lvalue:

template <class T> T* addressof(T& r) noexcept;

2. to_raw_pointer(p); // where:

template <class T>
inline
T*
to_raw_pointer(T* p) noexcept
{
    return p;
}

template <class Pointer>
inline
typename std::pointer_traits<Pointer>::element_type*
to_raw_pointer(Pointer p) noexcept
{
    return ::to_raw_pointer(p.operator->());
}

This calls a pointer's operator->(), which will either directly return a T* or forward to something that will either directly or indirectly return a T*. All pointer types should support operator->(), even if it is referencing a bool. A downside of this technique is that currently operator->() is required to not be called unless the pointer is dereferenecable. That restriction should be lifted in the standard.

In C++14 the return type of the second overload (well actually both overloads), can be conveniently replaced with auto.


If you have a T* and wish to construct a pointer, you are out of luck. There is no portable way to convert in this direction.


Also note this tangentially related LWG issue about the vector::data() member function's return type. It has bounced between value_type*, pointer and back, and is currently (and purposefully) value_type*.

like image 142
Howard Hinnant Avatar answered Sep 27 '22 19:09

Howard Hinnant