|
|||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||
|
Allocator Types
|
||||||||||||||||||
Allocator Types
C++ Report, June 1998
Allocators encapsulate memory allocation and are used by the container classes in the standard library for allocation and deallocation of the contained elements. "Container classes" in this context are the STL containers: vector, deque, list, (multi)set, and (multi)map, but also string, which is seen as containers of characters. Some container-related classes such as the container adapters stack, queue, and priority_queue, as well as string streams also use allocators. All remaining classes in the standard library do not use them at all. Purpose of AllocatorsAllocators were introduced to the standard library in order to allow for allocation strategies other than just heap data allocated by new and for alternative pointer types. Basically, an allocator covers two areas:
In addition to the flexibility gained regarding memory allocation strategies,
allocators are also intended for alternative pointer types. For example,
think of near, far, and huge pointers in certain memory models. Such non-standard
memory models introduce new pointer and reference types. Another category
of alternative pointer types are user-defined pointer classes such as smart
pointers or range-checked pointers. Allocators facilitate use of non-standard
pointer types, and the containers use these facilities in their implementation
and interface in all places where pointer and reference types are needed.
This way non-standard memory models and alternative pointer types can be
used in place of built-in C++ pointers.
An Overview of Allocator TypesThe C++ standard [ 1 ] requires that allocator types have a certain interface:public: // constructors, destructors, and the like allocator() throw(); allocator(const allocator&) throw(); template <class U> allocator(const allocator<U>&) throw(); ~allocator() throw(); // types and functions for alternative pointer types typedef allocator-specific pointer; typedef allocator-specific const_pointer; typedef allocator-specific reference; typedef allocator-specific const_reference; typedef allocator-specific value_type; typedef allocator-specific size_type; typedef allocator-specific difference_type; pointer address(reference x) const; const_pointer address(const_reference x) const; // types and functions for alternative memory allocation strategies pointer allocate(size_type, allocator<void>::const_pointer hint = 0); void deallocate(pointer p, size_type n); void construct(pointer p, const T& val); void destroy(pointer p); size_type max_size() const throw(); template <class U> struct rebind { typedef allocator<U> other; }; };
The standard library provides a standard allocator type, called allocator . It encapsulates allocation and deallocation of memory from the heap via operator new and uses the ordinary, built-in C++ pointer and reference types. The standard allocator is used as a default template argument in all templates that require an allocator type. The vector template, for example, is defined as: Use of Allocators in Containers Each container uses its allocator for memory allocation and deallocation of its elements and in all places where the container needs pointers, references, pointer differences, etc. that related to its elements. Here is an example taken from the vector interface: public: // types: typedef typename Allocator::reference reference; typedef typename Allocator::const_reference const_reference; typedef implementation defined iterator; typedef implementation defined const_iterator; typedef implementation defined size_type; typedef implementation defined difference_type; typedef T value_type; typedef Allocator allocator_type; typedef typename Allocator::pointer pointer; typedef typename Allocator::const_pointer const_pointer // element access: reference operator[](size_type n); const_reference operator[](size_type n) const; reference at(size_type n); const_reference at(size_type n) const; reference front(); const_reference front() const; reference back(); const_reference back() const; // ... }; The allocation and deallocation functions are used whenever elements are created, copied, or discarded. Here is an example from the vector's member function reserve() : { if (capacity() < newSize) { iterator newFirst = _allocator.allocate(newSize, this); uninitialized_copy(_first, _last, newFirst); for (iterator i = _first; i != _last; ++i) _allocator.destroy(&*i);} allocator.deallocate(_first); _endOfStorage = newFirst + newSize; _last = newFirst + size(); _first = newFirst; } } Details of Allocator TypesAfter this first overview, let us now take a more in-depth look at the functionality required of an allocator type. We will not discuss constructors, destructors, and the like, because they must have the expected functionality. More exciting are the nested types and member functions that an allocator type must provide. They fall into two categories: one part of the interface describes allocation strategies and the other part represents alternative pointer and reference types.Allocation StrategiesFor the purpose of alternative memory allocation schemes, an allocator type must define a number of member functions. These functions are supposed to reflect the functionality that is usually used by the run-time system for allocation, deallocation, construction, and destruction of objects. When the compiler sees a new expression then it decomposes this expression into two activities: memory allocation and object initialization. To put it simple, the allocation is performed by calling an allocation function, which is operator new() and can be overloaded. The initialization is performed by calling the right constructors. Similarly, deletion of an object falls into destruction and deallocation. Allocators allow the same decomposition. They define the functions allocate() and deallocate() for acquisition and release of (uninitialized) memory, and have functions construct() and destroy() for object initialization and cleanup. Here is the interface of the member functions required for alternative allocation strategies:public: pointer allocate(size_type, allocator<void>::const_pointer hint = 0); void deallocate(pointer p, size_type n); void construct(pointer p, const T& val); void destroy(pointer p); // ... }; The default allocator type allocator is an example of a conforming allocator type. Let us see how it implements these functions: public: pointer allocate(size_t size, allocator<void>::const_pointer hint = 0) { return (T*)::operator new(size*sizeof(T)); } void deallocate(pointer p, size_type n) { operator ::delete(p); } void construct(pointer p, const T& val) { new((void *)p) T(val); } void destroy(pointer p) { ((T*)p)->~T(); } }; There is an additional requirement to allocator types, that was introduced into the standard long after allocators had first been added to the standard library: All instances of a given allocator type are required to be interchangeable. This means that, for any two allocator objects of the same allocator type, it must not make a difference which allocator object is used for allocation or deallocation. This requirement imposes quite a restriction on allocator types. Consider the example of an allocator type thread_allocator representing thread-specific memory. Each thread allocator might manage memory specific to another thread. An allocator object of type thread_allocator will therefore contain an identification of the thread which the memory belongs to. Obviously, it makes a significant difference which allocator object is used. Objects of type thread_allocator are not interchangeable and violate the additional requirement. As a consequence, it is not guaranteed that this kind of allocator can be used with standard-compliant container implementations. Why is this restriction? Let us consider the "prohibited" situation of two containers with unequal allocator objects a little further. Some algorithms operates on two containers. An example of such an operation is the swap() algorithm. It exchanges the content of two objects. Both objects have to be of the same type. Say, they are vector s of string s, which are allocated via thread_allocator s. Both vector s are of the same type, namely vector<string, thread_allocator<string> > , but they have different, unequal allocator objects referring to different threads. vector<string, thread_allocator<string> > vec_2(thread_allocator(threadId_2)); vec_1.swap(vec_2); The requirement of interchangeability of allocator objects was introduced into the standard in order to allow for efficient library implementations. For a library implementor the additional requirement means that the swap() algorithm for instance can be implemented in its most efficient form: It does not make a difference how (i.e. by means of which particular allocator object) the vectors were allocated; hence swapping the internal pointers is sufficient. The effort for providing a swap() algorithm for containers with non-interchangeable allocators is significantly higher. The library implementor would have to provide several versions of swap() : an optimized one for interchangeable allocators and , maybe several additional, versions for non-interchangeable allocators. What does the requirement mean for users of the standard library? Well, it depends on what a user intends to do with allocators. Let's distinguish between implementing an allocator type and using allocators. We use allocators, implicitly, when we use containers. Imagine a situation similar to the swap() algorithm: we want to use several containers of the same type and create, copy, or destroy container elements. In that case we can take advantage of the additional requirement of interchangeable allocators: We need not care which container's allocator we use. The containers' allocators are all of the same type and, meeting to the requirement, are interchangeable and basically all the same.
The requirement of interchangeable allocators has a fundamentally different
meaning for programmers who aim to
implement
their own allocator
types. In that case the requirement is a restriction imposed on the new
allocator type. If the allocator type does not meet the requirement, then
there is no guarantee that it can be used with containers and algorithms
of any conforming standard library implementation.
Pointer and Reference Types
value_type pointer const_pointer reference const_reference size_type difference_type // functions: pointer address(reference x) const; const_pointer address(const_reference x) const;
Figure 2: Conversions of built-in pointer and reference types in the C++ type system Non-standard pointer and reference types must mimic this behavior: The types Allocator::value_type , Allocator::pointer , and Allocator::reference are required to be convertible to each other, in the same way as T , T* , and T& . This convertibility is achieved by imposing the following requirements on the types defined by an allocator:
Figure 3: Conversions of non-standard pointer and reference types defined by an allocator Let us see how the default allocator type allocator meets these requirements. The pointer, reference and related types are identical to those in the C++ type system. The address() function for conversion from a reference type to a pointer type boils down to the address operator. public: typedef T* pointer; typedef const T* const_pointer; typedef T& reference; typedef const T& const_reference; typedef T value_type; typedef size_t size_type; typedef ptrdiff_t difference_type; pointer address(reference x) const { return (&x); } const_pointer address(const_reference x) const { return (&x); } // ... }; Assume, we had defined an allocator type with non-standard pointer and reference types. Let us call it smartPtr_allocator . The non-standard types defined by smartPtr_allocator would be used, for instance, as the return type of the index operator[]() of a vector<T,smartPtr_allocator<T> > . Consider a function that takes a reference returned by operator[]() as a function argument, say we pick the max() algorithm from the standard library. To our very surprise, a call to max() like in the following code snippet ... max(vec[0],vec[1]) ... // error ... max< vector<int,smartPtr_allocator<int> >::value_type>(vec[0],vec[1]) ... This additional requirement was introduced into the standard to guarantee convenient use of standard library operations that use any of the types defined by allocators. Without the requirement, library implementers would have to find ways and means for allowing the desired convenience. They could, for instance, provide specializations of the max() algorithm, so that its use would not involve automatic argument deduction, but would work via function template overloading. Library implementers are encouraged, by the standards committee, to supply libraries that can accept allocator types with alternative pointer and reference types. To our knowledge, none of the available implementations does so. As a user of allocators, we profit from the imposed requirement, because we, too, can assume that a container's pointer or reference type indeed is a pointer or reference type in the sense of the C++ type system, and that conversions and deductions automatically performed by the compiler for these types work as expected.
If, however, we implement a new allocator type, the requirement basically
means that we cannot define alternative pointer and reference types at
all.
SummaryAllocators are intended to allow for alternative pointer and reference types as well as for alternative memory allocation schemes. Allocators are used by all containers in the standard library, including the character container class basic_string . The support of allocators is slightly restricted to permit efficient library implementations.ReferencesWorking Paper for Draft Proposed International Standard for Information SystemsProgramming Language C++ Accredited Standards Committee X3, INFORMATION PROCESSING SYSTEMS Doc No:X3J16/97-0079, WG21/N1117 Date: 29 September 1997
|
|||||||||||||||||||
© Copyright 1995-2003 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/C++Report/Allocators/Allocators.html> last update: 22 Oct 2003 |