THE STANDARD LIBRARIAN: What Are Allocators Good for?
Matt austern
Http://www.cuj.com/experts/1812/AUSTERN.HTM?topic=Experts
-------------------------------------------------- ------------------------------
Allocator is one of the most mysterious parts of the C language standard library. They are rarely explicitly explicit, and the standard does not explicitly make when they should be used. Today's allocator is very different from the initial STL recommendation, there are two other designs in this process - both depend on some of the characteristics, but until recently available on several compilers. For allocator features, the standard seems to have added commitments in some aspects, and in other respections revoke the commitment.
This column article will discuss what you can do with allocator and how to define your own version. I will only discuss the Allocator defined by the C standard: the design of the quasi-standard era, or try to bypass the defective compiler, will only increase confusion.
When do you not use allocator?
Allocator in the C standard is divided into two: a general demand set (described in § 20.1.5 (Table 32)), and Class of Std :: Allocator (described in §20.4.1). If a Class meets the needs of Table 32, we call it as an allocator. Std :: Allocator class meets those needs, so it is an allocator. It is the only pre-defined Allocator class in the standard library.
Each C programmer already knows the dynamic memory allocation: Write new x to assign memory and create a X-type new object, write the Delete P to destroy the object referred to P. And return its memory. You have reason to think that Allocator will use new and delete, but they don't. (C standard will :: operator new () as "allocation function", but very strange, allocator is not this.)
The most important fact about Allocator is that they are just for a purpose: encapsulates the low-level details of the STL container on memory management. You should not call all the member functions of Allocator directly in your code unless you are writing a STL container. You should not try to use Allocator to implement Operator New []; this is not allocator what to do. If you are not sure if you need to use allocator, don't use it.
Allocator is a class with allocate () and deallocate () member functions (equivalent to Malloc and Free). It also has a secondary function for maintenance of allocated memory and how to use these memory typef (pointer or reference type name). If a STL container uses the user's allocator to assign all the memory it needs (the predefined STL container can do this; they all have a template parameter, the default is std :: allocator, you can provide Your own allocator to control its memory management.
This flexibility is restricted: still determines how much memory it will apply and how to use themselves by the container. When the container is applied for more memory, you can control it to call the low-level function, but you can't let a vector action like a DEQUE by using allocator. Even so, sometimes this limited flexibility is also useful. For example, suppose you have a special FAST_ALLOCATOR that can quickly assign and release memory (perhaps by abandoning thread security, or using a small local heap), you can write down Std :: List
Define an allocator
You have seen this about Allocator: They are templates. Allocator, like a container, with value_type, and allocator's value_type must match the value_type of the container using it. This is sometimes more ugly: Map value_type is quite complex, so explicitly calls allocator's map looks like this, std :: map
Start with a simple example. According to C standard, std :: allocator is built on :: Operator new (). If you are using an automated memory to use tracking tools, let Std :: allocator make more easier. We can use malloc () instead of: Operator new (), and we don't think of (in good std :: allocator) complex performance optimization measures. We call this simple allocator as Malloc_allocator.
Since Malloc_allocator's memory management is simple, we can focus on the templates shared in all STLs Allocator. First, some types: Allocator is a class template, and its instance is specifically distributed for a type T. We provide a sequence of typedef to describe how to use this type of object: value_type refers to the T itself, and others have pointers and references with various modified words.
Template
{
PUBLIC:
Typedef t value_type;
TypeDef value_type * pointer;
Typedef const value_type * const_point;
TypeDef value_type & reason;
TypedEf const_type & const_reference;
Typedef std :: size_t size_type;
Typedef std :: ptrdiff_t Difference_type;
...
}
These types are very similar to the STL container, which is not a coincidence: the container class often directly extracts these types from its Allocator.
Why is there so many typedef? You may think that Pointer is redundant: it is value_type *. This is right, but you may sometimes want to define non-traditional allocator, its Pointer is a Pointer-Like's class, or non-standard vendor specific type value_type __far *; allocator is provided for non-standard extensions Standard HOOK. Unusual Pointer type is also the reason for the address () member function, which is only another way of writing in Malloc_allocator: Template
{
PUBLIC:
Pointer Address (Reference X) Const {Return & X;
const_pointer address (const_reference x) const {
Return & X;
}
...
}
Now we can start real work: allocate () and deallocate (). They are very simple, but not very like Malloc () and Free (). We pass it to allocate () two parameters: We are being assigned a number of objects to which space (Max_SIZE returns the maximum successful maximum requested value), and optional address values (can be used as a location prompt). Simple Allocator like Malloc_Allocator does not use that tips, but designed for high performance, allocator, may take advantage of it. The return value is a pointer to the memory block, which is sufficient to accommodate the N value_type type objects and have the correct alignment.
We also passed the two parameters: Of course, one is a pointer, but there is also an elemental count value. The container must master the size information yourself; the size parameters passing to allocate and deallocate must match. Similarly, this extra parameter is existing for efficiency, and like Malloc_allocator does not use it.
Template
{
PUBLIC:
Pointer Allocate (size_type n, const_pointer = 0) {
Void * p = std :: malloc (n * sizeof (t));
IF (! p)
THROW std :: bad_alloc ();
Return static_cast
}
Void Deallocate (Pointer P, size_type) {
Std :: free (p);
}
SIZE_TYPE MAX_SIZE () Const {
Return static_cast
}
...
}
Allocate () and deallocate () member functions are processed by unmelted memory, they do not construct and destroy objects. Statement a.allocate (1) is more like Malloc (INT) instead of new int. Before using the memory available from allocate (), you must create an object on this memory; you need to destroy those objects before you return to DEAllocate ().
The C language provides a mechanism to create an object at a specific memory location: Placement New. If you write new (p) t (a, b), then you are calling the constructor to generate a new object, as you write NEW T (A, B) or T T (A, B). The difference is that when you write new (p) t (a, b), you specify the location of the object being created: p The address pointed to by. (Nature, P must point to a large enough memory, and must be unused memory; you cannot build two different objects in the same address.). You can also call the destructor's destructor, without releasing memory, as long as you write P-> ~ T (). These features are rarely used, as usual memory allocation and initialization are performed: Using uninited memory is inconvenient and dangerous. One of the few things you need such a low level is that you are writing a container class, so Allocator will assign memory to initial decomposition. The member function construct () calls the Placement New, and the member function Destory () calls the destructor.
Template
{
PUBLIC:
Void Construct (Pointer P, Const value_type & x) {
NEW (P) value_type (x);
}
Void Destroy (POINTER P) {P-> ~ Value_Type ();
...
}
(Why all the allocator has those members function, when can the container can use the Placement New? One reason is to hide the clumsy syntax, and the other is if you write a more complex Allocator you might want to construct and destroy the object when construct () And DESTROY () has some other side effects. For example, allocator may maintain a log of all current active objects.)
These membership functions are not a Static, so the first thing to do before using Allocator is to create an Allocator object - that is, we should define some constructor. However, we don't need to assign a value: Once the container creates its allocator, this allocator will never want to change. The demand for Allocator in Table 32 does not include assignment. Just is based on security, in order to ensure that no one occasionally uses assignment, we will ban the functions that may automatically generate.
Template
{
PUBLIC:
Malloc_allocator () {}
Malloc_allocator (const malloc_allocator &) {}
~ malloc_allocator () {}
Private:
Void Operator = (const malloc_allocator);
...
}
These constructors do not actually do anything, because this allocator does not need to initialize any member variables. Based on the same reason, any two Malloc_allocator are interchangeable; if the types of A1 and A2 are malloc_allocator
Template
Inline Bool Operator == (Const Malloc_allocator
Const Malloc_allocator
}
Template
Inline Bool Operator! = (Const Malloc_allocator
Const malloc_allocator
Return False;
}
You will expect an allocator, is it different objects? Of course, it is difficult to provide a simple and useful example. A significant possibility is a memory pool. It is very common for large C processes, from several different locations ("pool") allocates memory, not what malloc () directly (). There are several benefits, one is IT Only Takes a Single Function Call to Reclaim All of the Memory Associated with a Particular Phase of The Program. Using a memory pool program may define tool functions such as MemPool_alloc and mempool_free, Mempol_alloc (n, p) allocate n bytes from pool P. It is easy to write a MMEPool_alocator to match such an architecture: Each Mempool_allocator object has a member variable to indicate which pool it is bound, and mempool_allocator :: allocate () will call MEMPOOL_ALLOC () to get memory from the appropriate pool. [Note 1]
Finally, we arrived at a subtle part of allocator: mapping between different types. The problem is, an allocator class, such as Malloc_allocator
This mechanism is called rebound, it has two parts. First, for a given value_type is the allocator type A1 of X1, you must be able to write an allocator type A2, which is identical to the A1, except that value_type is x2. Second, for a given A1 type object A1, you must be able to create an equivalent A2 type object A2. Both parts use member templates, which is the reasons why Allocator cannot be supported by old compiler or support.
Template
{
PUBLIC:
Template
Malloc_allocator (const malloc_allocator &) {}
Template
Struct rebind {typedef malloc_allocator ost;}; ...
}
This actually means an Allocator class that cannot be just a single class; it must be a family-related class, each has its own value_type. An Allocator class must have a Rebind member because it makes it possible to make another class from a class into the same society.
If there is an allocator type A1, the type corresponding to another value_type is TypenAme A1 :: Template Rebind
Incidentally, although most of the allocator has a template parameter (allocator's value_type), this is not the rules in the demand, but often happens. The rebound mechanism is also working well on the allocator of multi-template parameters:
Template
{
PUBLIC:
Template
Struct rebind {typedef my_allocator };
...
}
Finally, the last detail: What should we do for Void? Sometimes a container must involve a Void's pointer (again, we will study details in the next paragraph), and the heavy binding mechanism has almost give what we need, but not complete. It can't work because we will write code similar to malloc_allocator
Template <> Class Malloc_allocator
{
TypeDef void value_type;
TypeDef void * Pointer;
TypedEf const void * const_point;
Template
Struct rebind {typedef malloc_allocator };
It's it! The complete source code of Malloc_allocator is Listing 1.
Use allocator
The easiest way to use allocator, of course, to pass them as a parameter to the container class;
Std :: Vector
Replace simple std :: vector
TypedEf std :: list
Substitution Simple std :: list
But you can do more. STL's selling point is that it is scalability: as you can write your own allocator, you can also write your own container class. If you are very careful, and you write the container class using its allocator to process all memory related operations, others will be able to join their own user-defined Allocator.
Containers such as std :: Vector and Std :: List are very complex, and most of the complexity is independent of memory management. Let us start with a simple example so that we can only pay attention to allocator. Considering a class of fixed size arrays, the number of array, elements is set in the constructor, and will not change after this. (This is a simplified version of Std :: Valarray.) It has two template parameters, element types, and allocator types.
Containers, like ALLOCATOR, with a nest type type statement: value_type, reason, const_reference, size_type, difference_type, iterator, and const_iterator. Typically, most of these types can be obtained directly from its allocator - this also explains why the Value_Type of the container must match the Allocator.
Of course, the Iterator type is usually not from allocator; usually Iterator is a class, which depends entirely on the inner representation of the container. The Array spectrum is simpler than the usual container because it actually stores all elements in units of continuous memory; we only need to maintain two pointers that point to memory blocks and ends. Iterator is a pointer.
Before further, we must decide: How will we save allocator? The constructor will accept an Allocator object as a parameter. We must save a copy of its copy within the entire life of the container, because it also needs it in the destructor.
I feel, there is no problem here: we just declare a member variable of an Allocator type and then use it. The method is correct, but not cool. After all, in 99% time, users don't want to consider things about Allocator; they only write Array
Many ways to solve this problem, some of which use traits and offset. Perhaps the simplest solution is to use (private) inheritance rather than member variables. The compiler is allowed to optimize the empty base, and the vast majority of the compilers are done.
We can finally write down the defined skeleton:
Template
Class Array: Private Allocator
{
PUBLIC:
Typedef t value_type;
TypedEf TypeName Allocator :: Reference Reference;
Typedf TypeName Allocator :: Const_Reference
Const_reference;
TypedEf TypeName Allocator :: Size_Type Size_Type;
TypedEf TypeName Allocator :: Difference_Type
Difference_type;
Typedf Typename Allocator :: Pointer Iterator
TypeDef Typename Allocator :: Const_Pointer Const_Ipectrator
TypeDef allocator allocator_type;
Allocator_type get_allocator () const {
Return static_cast
}
Iterator Begin () {Return First;
Iterator end () {return Last;}
Const_iterator begin () const {returnfirst1;}
Const_iterator End () const {return last;}
Array (size_type n = 0,
Const T & X = T (),
Const allocator & a = allocator ());
Array (Const Array &);
~ Array ();
Array & Operator = (Const Array &);
Private:
Typename Allocator :: Pointer First;
TypeName Allocator :: Pointer Last;
}
If you want to meet the needs of the container (the demand is available in C standard §23.1, Table 65), which is not all of our needs, but most of them are completely unrelated to Allocator. For our purposes, the most interested member function is a constructor (which assigns memory and creation objects), and the destruction function (it destroes memory and releases memory).
The constructor initializes the Allocator base class to get a piece of memory that is sufficient to accommodate n elements (if we are writing something similar to vector, we may apply for greater memory for growth), then traverse memory to create a copy of the initialization value . The only problem is unusual security: if a constructor of an element throws an exception, we must revoke everything we do.
Template
Array
Const T & X,
Const Allocator & A)
: Allocator (A), First (0), Last (0)
{
IF (n! = 0) {
First = allocator :: allocate (n);
SIZE_TYPE I;
Try {
FOR (i = 0; i Allocator :: Construct (First i, x); } } Catch (...) { For (SIZE_TYPE J = 0; J
Allocator :: DESTROY (First J); } Allocator :: Deallocate (First, N); throw; } } } (You may ask why we have written this loop; std :: uninitialized_fill () is not what we need? Almost, but not exactly the same. We must call allocator constructor's function instead of simple pacement new. Maybe The future C standard will contain a uninitialized_fill () function that accepts allocator as the parameter, so that this explicit loop is no longer needed. The destructor is relatively simple, because we don't need to worry about abnormal security: T 's destructor is assumed to never throw an exception. Template Array { IF (first! = last) { Iterator i = first; i Allocator :: Destroy (i); Allocator :: DEAllocate (First, Last - First); } } Our simple Array class does not need to use heavy binding or conversion, but this is just because Array Template Struct List_node { T Val; List_node * next; } The process of adding a new value to the list looks like this: l Using a value_type is the allocator of list_node l Use a value_type to be the allocator of t, build a new element at the VAL location of the node. l Connect this node to the appropriate location. This process needs to handle two different allocator, one of which is obtained by rebounding the other. It works very well for almost all programs, even if you use allocator to use the programs that are relatively complex. It cannot be completed is to provide allocator with some unusual pointer types. It obviously depends on the ordinary pointer that can use the list_node If you are extremely ambitious, you want to pass other possible pointer types to allocator, everything suddenly has more complicated - pointing from one node to another node no longer List_Node Template Struct List_node { T Val; Pointer next; } Template Class List: Private Alloc { Private: Typedef TypeName Alloc :: Template Rebind Void_alloc; typedef typename void_alloc :: Pointer voidptr; TypedEf Typename List_Node Typedef Typename Alloc :: Template Rebind Node_alloc; TypeDef Typename Node_alloc :: Pointer Nodeptr TypeDef TypeName Alloc :: Template Rebind Voidptr_alloc; Nodeptr new_node (const t & x, nodeptr next) { Alloc a = GET_ALLOCATOR (); NodePtr P = Node_alloc (a) .allocate (1); Try { A.Construct (P-> VAL, X); } Catch (...) { Node_alloc (a) .deallocate (P, 1); Throw; } Voidptr_alloc (a) .construct (p-> next, voidptr (next)); Return P; } ... } Finally, in order to prevent such a small benefit, it is too worthless, remind: Just because you can write a container that uses Allocator, it doesn't mean you must, or you should. Sometimes you may write a container class that relies on a special memory allocation policy, such as complex to disk-based B-tree containers or a Block class described in my book. Even if you really want to write a container class that uses Allocator, you don't have to support possible pointer types. You can write a container, and require all user-defined Allocator uses a normal pointer and clearly describes this limit in the document. Not everything is fully universal. Looking forward to the future If you want to write a simple allocator like Malloc_allocator, there should be no difficulty. (The premise is that you are using a comparative modern compiler). However, if your heart is relatively large, the ALLOCATOR-based allocator-based allocator is supported by the memory pool. What do you have to support if you want to use a Pointer-like type? Does it have a NULL value? If so, how should the value written? Can you use type conversion? How do you convert between class pointer objects and normal pointer? Do you have to consider an abnormality thrown when the pointer is? I put forward some assumptions in this last section; the C standard did not specify the right or wrong of these hypothetics. These details have been left to the implementation of the specific standard library, even if an implementation completely ignored the optional pointer type is legal. The C standard also left some problems with no answers, such as what will happen when a different instance of an allocator is uncharged. Fortunately, the situation is not as terrible as the words in the standard (§ 20.1.5, para. 4-5). The standard left no answer is because the C Standardization Commission does not consensus on the answer; the necessary experience of Allocator does not exist. Every person involved is considered to be a temporary patch, which is definitely removed in the future revision. Wait a minute, if you care about the portability, it is best to stay away from the optional pointer type, and if you are willing to accept some restrictions, you can safely use allocator, such as MemPool_Allocator, and a large difference in different object entities. The implementation of all mainstream standard libraries now does not support such allocator in a certain aspect, and the difference between different implementations is not large. As the container accepts an Allocator as a template parameter, the constructor of the container also accepts an allocator as a parameter. The container retains a copy of this parameter and uses this copy to handle all memory management; the container's Allocator is completed in the constructor, it will remain unchanged. The only problem is what happens when you run a collaborative operation in memory management. There is indeed such operations in the standard library: SWAP () (all containers) and std :: list :: splice (). In principle, you can handle them in such a variety of different ways: l Disable SWAP () and splice () between two Allocator not equivalent containers. l Add an allocator equivalent test in swap () and splice (). If the inequality, the reduction is called Copy () and assignment. l Only SWAP (): As with the data in the container, exchange their allocator. (It is difficult to find how to extend it to splice. It also brings a problem: How to swap does not define what assignment operator?) If you can avoid using swap () and splice (), everything is safe to use SWAP () and splice (), everything is safe. In practice, I didn't find that this will cause serious binding: You need to strictly practice safe use of such features such as memory pools, and you may not want to mix the container that uses different Allocator without selecting. Part is not familiar, part is not satisfactory because of the requirements of C standards, and the most use of Allocator is very simple. Since the C community has become more familiar with Allocator, and the standard has been clarified, we can look forward to more complex use will emerge. Listing 1: a sample allocator based on malloc Template { PUBLIC: Typedef t value_type; TypeDef value_type * pointer; Typedef const value_type * const_point; TypeDef value_type & reason; TypedEf const_type & const_reference; Typedef std :: size_t size_type; Typedef std :: ptrdiff_t Difference_type; Template Struct rebind {typedef malloc_allocator }; Malloc_allocator () {} Malloc_allocator (const malloc_allocator &) {} Template Malloc_allocator (const malloc_allocator &) {} ~ malloc_allocator () {} Pointer Address (Reference X) Const {Return & X;} const_pointer address (const_reference x) const { Return X; } Pointer Allocate (size_type n, const_pointer = 0) { Void * p = std :: malloc (n * sizeof (t)); IF (! p) THROW std :: bad_alloc (); Return static_cast } Void deallocate (POINTER P, SIZE_TYPE) {std :: free (p);} SIZE_TYPE MAX_SIZE () Const { Return static_cast } Void Construct (Pointer P, Const value_type & x) { NEW (P) value_type (x); } Void Destroy (POINTER P) {P-> ~ Value_Type (); Private: Void Operator = (const malloc_allocator); } Template <> Class Malloc_allocator { TypeDef void value_type; TypeDef void * Pointer; TypedEf const void * const_point; Template Struct rebind {typedef malloc_allocator }; } Template Inline Bool Operator == (Const Malloc_allocator Const malloc_allocator Return True; } Template Inline Bool Operator! = (Const Malloc_allocator Const malloc_allocator Return False; } Note: [1] You can see an Example of a pool allocator in the open source sgi pro64tm compiler, http://oss.sgi.com/projects/pro64/.