cpp11 library

Save to:
Instapaper Pocket Readability

C++11 Standard Library Extensions — General Libraries

unique_ptr

unique_ptr (defined in <memory>) provides a semantics of strict ownership:

  • owns the object it holds a pointer to
  • is not CopyConstructible, nor CopyAssignable, however it is MoveConstructible and MoveAssignable.
  • stores a pointer to an object and deletes that object using the associated deleter when it is itself destroyed (such as when leaving block scope (6.7)).

The uses of unique_ptr include:

  • providing exception safety for dynamically allocated memory
  • passing ownership of dynamically allocated memory to a function
  • returning dynamically allocated memory from a function
  • storing pointers in containers

unique_ptr is almost exactly as efficient as using a raw pointer, but with safe ownership semantics. It’s “what auto_ptr should have been” (but that we couldn’t write in C++98).

unique_ptr is a move-only type, and so relies critically on rvalue references and move semantics.

Here is a conventional 20th-century piece of exception unsafe code:

    X* f()
    {
        X* p = new X;
        // do something - maybe throw an exception
        return p;
    }

A solution is to hold the pointer to the object on the free store in a unique_ptr:

    X* f()
    {
        unique_ptr<X> p(new X);     // or {new X} but not = new X
        // do something -- maybe throw an exception
        return p.release();
    }

Now, if an exception is thrown, the unique_ptr will (implicitly) destroy the object pointed to. That’s basic RAII. However, unless we really need to return a built-in pointer, we can do even better by returning a unique_ptr:

    unique_ptr<X> f()
    {
        unique_ptr<X> p(new X);     // or {new X} but not = new X
        // do something -- maybe throw an exception
        return p;   // the ownership is transferred out of f()
    }

We can use this f like this:

    void g()
    {
        unique_ptr<X> q = f();       // move using move constructor
        q->memfct(2);                // use q
        X x = *q;                    // copy the object pointed to
        // ...
    }   // q and the object it owns is destroyed on exit

The unique_ptr has “move semantics” so the initialization of q with the rvalue that is the result of the call f() simply transfers ownership into q.

One of the uses of unique_ptr is as a pointer in a container that owns its heap-allocated objects, where in the past we might have used a built-in pointer except for exception safety problems (and to guarantee destruction of the pointed to elements):

    vector<unique_ptr<string>> vs { new string{"Doug"}, new string{"Adams"} };

unique_ptr is represented by a simple built-in pointer and the overhead of using one compared to a built-in pointer are miniscule. In particular, unique_ptr does not offer any form of dynamic checking.

See also:

  • the C++ draft section 20.7.10
  • Howard E. Hinnant: unique_ptr Emulation for C++03 Compilers.

shared_ptr

A shared_ptr is used to represent shared ownership; that is, when two pieces of code need access to some data but neither has exclusive ownership (in the sense of being responsible for destroying the object). A shared_ptr is a reference counted pointer where the object pointed to is deleted when the use count goes to zero. Here is a highly artificial example:

    void test()
    {
        shared_ptr<int> p1(new int);    // count is 1
        {
            shared_ptr<int> p2(p1); // count is 2
            {
                shared_ptr<int> p3(p1); // count is 3
            }   // count goes back down to 2
        } // count goes back down to 1
    }   // here the count goes to 0 and the int is deleted.

A more realistic example would be be pointers to nodes in a general graph where someone wanting to remove a pointer to a node wouldn’t know if anyone else held a pointer to that node. If a node can hold resources that require an action by a destructor (e.g. a file handle so that a file needs to be closed when the node is deleted). You could consider shared_ptr to be for what you might consider plugging in a garbage collector for, except that maybe you don’t have enough garbage for that to be economical, your execution environment doesn’t allow that, or the resource managed is not just memory (e.g. that file handle) so that you need release to be deterministic when the last user goes away instead of done lazily at some nondeterminstic time, and ordered so that the object’s destructor can safely use other heap objects. For example:

    struct Node {   // note: a Node may be pointed to from several other nodes.
        shared_ptr<Node> left;
        shared_ptr<Node> right;
        File_handle f;
        // ...
    };

Here Node’s destructor (the implicitly generated destructor will do fine) deletes its sub-nodes; that is, left and right’s destructors are invoked. When this node is destroyed, since left is a shared_ptr, the Node pointed to (if any) is deleted if left was the last pointer to it; right is handled similarly and f’s destructor does whatever is required for f.

Note that you should not use a shared_ptr just to pass a pointer from one owner to another; that’s what unique_ptr is for and unique_ptr does that cheaper and better. If you have been using counted pointers as return values from factory functions and the like, consider upgrading to unique_ptr rather than shared_ptr.

Please don’t thoughtlessly replace pointers with shared_ptrs in an attempt to prevent memory leaks; shared_ptrs are not a panacea nor are they without costs:

  • a circular linked structure of shared_ptrs will cause a memory leak (you’ll need some logical complication to break the circle, e.g. using a weak_ptr),
  • “shared ownership objects” tend to stay “live” for longer than scoped objects (thus causing higher average resource usage),
  • heavy manipulation of shared pointers themselves (such as transferring ownership around frequently, though this is an antipattern) can be expensive in a multi-threaded environment because of the need to avoid data races on the use count,
  • the algorithms/logic for the update of any shared object is easier to get wrong than for an object that’s not shared.

A shared_ptr represents shared ownership but shared ownership isn’t always ideal: By default, it is better if an object has a definite owner and a definite, predictable lifespan. Prefer the following in order: stack or member lifetime (stack variables or by-value member variables); heap allocation with unique_ptr unique ownership; and heap allocation via make_shared with shared ownership.

See also:

weak_ptr

weak_ptrs are for shared observation just as shared_ptrs are for shared ownership. weak_ptrs are commonly known to be what you need to break cycles in data structures managed using shared_ptrs, but more generally it is better to think of a weak_ptr as a pointer to something that

  1. you need access to (only) if it exists, and
  2. may get deleted (by someone else), and
  3. must have its destructor called after its last use (usually to delete anon-memory resource)

Consider an implementation of the old “asteroid game”. All asteroids are owned by “the game” but each asteroids must keep track of neighboring asteroids and handle collisions. A collision typically leads to the destruction of one or more asteroids. Each asteroid must keep a list of other asteroids in its neighborhood. Note that being on such a neighbor list should not keep an astroid “alive” (so a shared_ptr would be inappropriate). On the other hand, an asteroid must not be destroyed while another asteroid is looking at it (e.g. to calculate the effect of a collision). And obviously, an asteroid’s destructor must be called to release resources (such as a connection to the graphics system). What we need is a list of asteroids that might still be intact and a way of “getting hold of one if it exists” for a while. A weak_ptr does just that:

    void owner()
    {
        // ...
        vector<shared_ptr<Asteroid>> va(100);
        for (int i=0; i<va.size(); ++i) {
            // ... calculate neighbors for new asteroid ...
            va[i].reset(new Asteroid(weak_ptr<Asteroid>(va[neighbor]));
            launch(i);
        }
        // ...
    }

reset() is the function to make a shared_ptr refer to a new object.

Obviously, this example code radically simplified “the owner” and gave each new Asteroid just one neighbor. The key is that we give the Asteroid a weak_ptr to that neighbor. The owner keeps a shared_ptr to represent the ownership that’s shared whenever an Asteroid is looking (but not otherwise). The collision calculation for an Asteroid will look something like this:

    void collision(weak_ptr<Asteroid> p)
    {
        if (auto q = p.lock()) {    // p.lock returns a shared_ptr to p's object
            // ... that Asteroid still existed: calculate ...
        }
        else {
            // ... oops: that Asteroid has already been destroyed: just forget about it (delete the weak_ptr to it ...
        }
    }

Note that even if the owner decides to shut down the game and deletes all Asteroids (by destroying the shared_ptrs representing ownership), every Asteroid that is in the middle of calculating a collision still finishes correctly because after the p.lock() it holds a shared_ptr that will ensure the Asteroid stays alive for at least as long as collision is using it via that shared_ptr.

You should expect to find weak_ptr use much rarer than “plain” shared_ptr use, and both of those to be rarer than unique_ptr which should be most popular of all as it represents a simpler (and more efficient) notion of ownership and (therefore) allows better local reasoning.

See also:

  • the C++ draft: weak_ptr (20.7.13.3)

Garbage collection ABI

Garbage collection (automatic recycling of unreferenced regions of memory) is optional in C++; that is, a garbage collector is not a compulsory part of an implementation. However, C++11 provides a definition of what a GC can do if one is used and an ABI (Application Binary Interface) to help control its actions.

The rules for pointers and lifetimes are expressed in terms of “safely derived pointer” (3.7.4.3); roughly: “pointer to something allocated by new or to a sub-object thereof.” Here are some examples of “not safely derived pointers” aka “disguised pointers” aka what not to do in a program you want to be considered well behaved and comprehensible to ordinary mortals:

  • Make a pointer point “elsewhere” for a while
    int* p = new int;
    p+=10;
    // ... collector may run here ...
    p-=10;
    *p = 10;    // can we be sure that the int is still there? 
  • Hide the pointer in an int
    int* p = new int;
    int x = reinterpret_cast<int>(p);   // non-portable
    p=0;
    // ... collector may run here ...
    p = reinterpret_cast<int*>(x);
    *p = 10;    // can we be sure that the int is still there?
  • There are many more and even nastier tricks. Think I/O, think “scattering the bits around in different words”, …

There are legitimate reasons to disguise pointers (e.g. the xor trick in exceptionally memory-constrained applications), but not as many as some programmers think.

A programmer can specify where there are no pointers to be found (e.g. inside a JPEG image) and what memory can’t be reclaimed even if the collector can’t find a pointer into it:

    void declare_reachable(void* p);    // the region of memory starting at p
                        // (and allocated by some allocator
                        // operation which remembers its size)
                        // must not be collected
    template<class T> T* undeclare_reachable(T* p);

    void declare_no_pointers(char* p, size_t n);       // p[0..n] holds no pointers
    void undeclare_no_pointers(char* p, size_t n);

A programmer can inquire which rules for pointer safety and reclamation is in force:

    enum class pointer_safety {relaxed, preferred, strict };
    pointer_safety get_pointer_safety();

3.7.4.3[4]: If a pointer value that is not a safely-derived pointer value is dereferenced or deallocated, and the referenced complete object is of dynamic storage duration and has not previously been declared reachable (20.7.13.7), the behavior is undefined.

  • relaxed: safely-derived and not safely-derived pointers are treated equivalently; like C and C++98, but that was not my intent - I wanted to allow GC if a user didn’t keep a valid pointer around for an object.
  • preferred: like relaxed; but a garbage collector may be running as a leak detector and/or detector of dereferences of “bad pointers”
  • strict: safely-derived and not safely-derived pointers may be treated differently, i.e. a garbage collector may be running and will ignore pointers that aren’t safely derived

There is no standard way of saying which alternative you prefer. Considered that a “quality of implementation” and a “programming environment” issue.

See also:

tuple

The standard library tuple (an N-tuple) is a ordered sequence of N values where N can be a constant from 0 to a large implementation-defined value, defined in <tuple>. You can think of a tuple as an unnamed struct with members of the specified element types. In particular, the elements of a tuple are stored compactly; a tuple is not a linked structure.

The element types of a tuple can explicitly specified or be deduced (using make_tuple()) and the elements can be access by (zero-based) index using get():

    tuple<string,int> t2("Kylling",123);

    auto t = make_tuple(string("Herring"),10, 1.23);    // t will be of type tuple<string,int,double>
    string s = get<0>(t);
    int x = get<1>(t);
    double d = get<2>(t);

Tuples are used (directly of indirectly) whenever we want a heterogeneous list of elements at compile time but do not want to define a named class to hold them. For example, tuple is used internally in std::function and std::bind to hold arguments.

The most frequently useful tuple is the 2-tuple; that is, a pair. However, pair is directly supported in the standard library through std::pair (20.3.3 Pairs). A pair can be used to initialize a tuple, but the opposite isn’t the case.

The comparison operators (==, !=, <, <=, >, and >=) are defined for tuples of comparable element types.

See also:

Type traits

See:

function and bind

Note: function is long-term useful. However, bind is almost entirely superseded by C++14 lambdas with generalized lambda capture, but it has a few advantages if your compiler supports only C++11 lambdas and can be more compact in basic cases.

The bind and function standard function objects are defined in <functional> (together with a lot of other function objects); they are used to handle functions and function arguments. bind is used to take a function (or a function object or anything you can invoke using the (a,b,c) syntax) and produce a function object with one or more of the arguments of the argument function “bound” or rearranged. For example:

    int f(int,char,double);

    auto ff = bind(f,_1,'c',1.2);   // deduce return type
    int x = ff(7);          // f(7,'c',1.2);

    // equivalent with lambdas
    auto ff2 = [](int i){ f(i,'c',1.2); };  // deduce return type
    int x2 = ff2(7);            // f(7,'c',1.2);

This binding of arguments is usually called “Currying.” The _1 is a placeholder object indicating where the first argument of ff is to go when f is called through ff. The first argument is called _1, the second _2, and so on. For example:

    int f(int,char,double);

    auto frev = bind(f,_3,_2,_1);   // reverse argument order
    int x = frev(1.2,'c',7);    // f(7,'c',1.2);

    // equivalent with lambdas
    auto frev2 = [](double d, char c, int i){ bind(f,i,c,d); }; // reverse argument order
    int x2 = frev2(1.2,'c',7);  // f(7,'c',1.2);

Note how auto saves us from having to specify the type of the result of bind.

If the function to be called is overloaded, it is not possible to just bind arguments. Instead, we have to explicitly state which version of an overloaded function we want to bind:

    int g(int);
    double g(double);   // g() is overloaded

    auto g1 = bind(g,_1);               // error: which g()?
    auto g2 = bind((double(*)(double))g,_1);    // ok (but ugly)

    // equivalent with C++11 lambdas, which handle this naturally
    auto g3 = [](double d){ g(d); };    // ok in C++11

    // both shorter and more powerful with C++14 lambdas
    auto g4 = [](auto x){ g(x); };  // ok in C++14, and gives full access to the overload set

bind() comes in two variants: the one shown above and a “legacy” version where you explicitly specify the return type:

    auto f2 = bind<int>(f,7,'c',_1);    // explicit return type
    int x = f2(1.2);            // f(7,'c',1.2);

This second version was necessary in C++98 and is widely used because the first (and for a user simplest) version cannot be implemented in C++98.

function is a type that can hold a value of just about anything you can invoke using the (a,b,c) syntax, including allowing conversions on the parameters and the return type, making it a very flexible facility indeed while preserving strict type-safety. In particular, the result of bind can be assigned to a function. function is very simple to use. For example:

    function<float (int x, int y)> f;   // make a function object

    struct int_div {            // take something you can call using ()
        float operator()(int x, int y) const { return ((float)x)/y; };
    };

    f = int_div();              // assign
    cout << f(5, 3) << endl;        // call through the function object
    std::accumulate(b,e,1,f);       // passes beautifully

Member functions can be treated as free functions with an extra “explicit this” argument:

    struct X {
        int foo(int);
    };

    function<int (X*, int)> f;
    f = &X::foo;        // pointer to member

    X x;
    int v = f(&x, 5);   // call X::foo() for x with 5
    function<int (int)> ff = std::bind(f,&x,_1);    // first argument for f is &x
    v=ff(5);        // call x.foo(5)

functions are useful for callbacks, for passing operations as argument, etc. function can be seen as a replacement for the C++98 standard library function objects mem_fun_t, pointer_to_unary_function, etc. Similarly, bind() can be seen as a replacement for bind1st() and bind2nd().

See also:

Regular Expressions

To be written.

In the meantime, see:

Time utilities

We often want to time things or to do things dependent on timing. For example, the standard-library mutexes and locks provide the option for a thread to wait for a period of time (a duration) or to wait until a given point in time (a time_point).

If you want to know the current time_point, you can call now() for one of three clocks: system_clock, steady_clock, and high_resolution_clock. For example:

    steady_clock::time_point t = steady_clock::now();
    // do something
    steady_clock::duration d = steady_clock::now() - t;
    // something took d time units

A clock returns a time_point, and a duration is the difference between two time_points from the same clock. As usual, if you are not interested in details, auto is your friend:

    auto t = steady_clock::now();
    // do something
    auto d = steady_clock::now() - t;
    // something took d time units

The time facilities here are intended to efficiently support uses deep in the system; they do not provide convenience facilities to help you maintain your social calendar. In fact, the time facilities originated with the stringent needs for high-energy physics. To be able to express all time scales (such as centuries and picoseconds), avoid confusion about units, typos, and rounding errors, durations and time_points are expressed using a compile-time rational number package. A duration has two parts: a numeric clock “tick” and something (a “period”) that says what a tick means (is it a second or a millisecond?); the period is part of a duration’s type. The following table from the standard header <ratio>, defining the periods of the SI system (also known as MKS or metric system) might give you an idea of the scope of use:

    // convenience SI typedefs:
    typedef ratio<1, 1000000000000000000000000> yocto;  // conditionally supported
    typedef ratio<1,    1000000000000000000000> zepto;  // conditionally supported
    typedef ratio<1,       1000000000000000000> atto;
    typedef ratio<1,          1000000000000000> femto;
    typedef ratio<1,             1000000000000> pico;
    typedef ratio<1,                1000000000> nano;
    typedef ratio<1,                   1000000> micro;
    typedef ratio<1,                      1000> milli;
    typedef ratio<1,                       100> centi;
    typedef ratio<1,                        10> deci;
    typedef ratio<                       10, 1> deca;
    typedef ratio<                      100, 1> hecto;
    typedef ratio<                     1000, 1> kilo;
    typedef ratio<                  1000000, 1> mega;   
    typedef ratio<               1000000000, 1> giga;
    typedef ratio<            1000000000000, 1> tera;
    typedef ratio<         1000000000000000, 1> peta;
    typedef ratio<      1000000000000000000, 1> exa;    
    typedef ratio<   1000000000000000000000, 1> zetta;  // conditionally supported
    typedef ratio<1000000000000000000000000, 1> yotta;  // conditionally supported

The compile time rational numbers provide the usual arithmetic (+, -, *, and /) and comparison (==, !=, <, <=, >, >=) operators for whatever combinations durations and time_points makes sense (e.g. you can’t add two time_points). These operations are also checked for overflow and divide by zero. Since this is a compile-time facility, don’t worry about run-time performance. In addition you can use ++, --, +=, -=, *=, and /= on durations and tp+=d and tp-=d for a time_point tp and a duration d.

Here are some examples of values using standard duration types as defined in <chrono>:

    microseconds mms = 12345;
    milliseconds ms = 123;
    seconds s = 10;
    minutes m = 30;
    hours h = 34;

    auto x = std::chrono::hours(3);         // being explicit about namespaces
    auto x = hours(2)+minutes(35)+seconds(9);   // assuming suitable "using"

You cannot initialize a duration to a fraction. For example, don’t try 2.5 seconds; instead use 2500 milliseconds. This is because a because a duration is interpreted as a number of “ticks.” Each tick represents one unit of the duration’s “period,” such as milli and kilo as defined above. The default unit is seconds; that is, for a duration with a period of 1 a tick is interpreted as a second. We can be explicit about the representation of a duration:

    duration<long> d0 = 5;          // seconds (by default)
    duration<long,kilo> d1 = 99;        // kiloseconds!
    duration<long,ratio<1000,1>> d2 = 100;  // d1 and d2 have the same type ("kilo" means "*1000")

If we actually want to do something with a duration, such as writing it out, we have to give a unit, such as minutes or microseconds. For example:

    auto t = steady_clock::now();
    // do something
    nanoseconds d = steady_clock::now() - t;    // we want the result in nanoseconds
    cout << "something took " << d << "nanoseconds\n";

Alternatively, we could convert the duration to a floating point number (to get rounding):

    auto t = steady_clock::now();
    // do something
    auto d = steady_clock::now() - t;
    cout << "something took " << duration_cast<double>(d).count() << "seconds\n";

The count() is the number of “ticks.”“

See also:

  • Standard: 20.9 Time utilities [time]
  • Howard E. Hinnant, Walter E. Brown, Jeff Garland, and Marc Paterno: A Foundation to Sleep On. N2661=08-0171. Including “A Brief History of Time” (With apologies to Stephen Hawking).

Random number generation

Random numbers are useful in many contexts, such as testing, games, simulation, and security. The diversity of application areas is reflected in the wide selection of random number generation utilities provided by the standard library. A random number generator consists of two parts: (1) an engine that produces a sequence of random or pseudo-random values, and (2) a distribution that maps those values to a mathematical distribution in a range. Examples of distributions are uniform_int_distribution (where all integers produced are equally likely) and normal_distribution (“the bell curve”), each for some specified range. For example:

    uniform_int_distribution<int> one_to_six {1,6};  // distribution that maps to the ints 1..6
    default_random_engine re {};                     // the default engine

To get a random number, you call a distribution with an engine:

    int x = one_to_six(re); // x becomes a value in [1:6]

To avoid passing the engine in every call, we could bind that argument to get a function object that’s callable without arguments:

    auto dice {bind(one_to_six,re)};   // make a generator

    int x = dice(); // roll the dice: x becomes a value in [1:6]

(Thanks to its uncompromising attention to generality and performance, one expert deemed the standard-library random number component “what every random number library wants to be when it grows up.)

What if we just want something simple to use like:

    int rand_int(int low, int high);    // generate a random number from a uniform distribution in [low:high]

So, how could we get that? We have to put something like dice() inside rand_int():

    int rand_int(int low, int high)
    {
        static default_random_engine re {};
        using Dist = uniform_int_distribution<int>;
        static Dist uid {};
        return uid(re, Dist::param_type{low,high});
    }

While providing that definition needs a bit of expertise, calling rand_int() is manageable in even the first week of a C++ course.

Just to show a non-trivial example, here is a program that generates and prints a normal distribution’s histogram:


#include <iostream> #include <random> #include <vector> std::default_random_engine re; // the default engine std::normal_distribution<double> nd(31 /* mean */, 8 /* sigma */); auto norm = std::bind(nd, re); std::vector<int> mn(64); int main() { for (int i = 0; i<1200; ++i) ++mn[round(norm())]; // generate for (int i = 0; i<mn.size(); ++i) { std::cout << i << '\t'; for (int j=0; j<mn[i]; ++j) std::cout << '*'; std::cout << '\n'; } }

The result of one execution was:

0   
1   
2   
3   
4   *
5   
6   
7   
8   
9   *
10  ***
11  ***
12  ***
13  *****
14  *******
15  ****
16  **********
17  ***********
18  ****************
19  *******************
20  *******************
21  **************************
22  **********************************
23  **********************************************
24  ********************************************
25  *****************************************
26  *********************************************
27  *********************************************************
28  ***************************************************
29  ******************************************************************
30  **********************************************
31  *********************************************************************
32  **********************************************
33  *************************************************************
34  **************************************************************
35  ***************************************
36  ***********************************************
37  **********************************************
38  *********************************************
39  ********************************
40  ********************************************
41  ***********************
42  **************************
43  ******************************
44  *****************
45  *************
46  *********
47  ********
48  *****
49  *****
50  ****
51  ***
52  ***
53  **
54  *
55  *
56  
57  *
58  
59  
60  
61
62
63

See also:

  • Standard 26.5: Random number generation
  • Walter E. Brown: Random Number Generation in C++11 – note, normally committee papers are not tutorials, and it’s not often you get an excellent tutorial written by a world-class expert who is also the designer of the library – strongly recommended as a go-to source of information about <random>

Scoped allocators

For compactness of container objects and for simplicity, C++98 did not require containers to support allocators with state: Allocator objects need not be stored in container objects. This is still the default in C++11, but it is possible to use an allocator with state, say an allocator that holds a pointer to an arena from which to allocate. For example:

template<class T> class Simple_alloc {  // C++98 style
    // no data
    // usual allocator stuff
};

class Arena {
    void* p;
    int s;
public:
    Arena(void* pp, int ss);
    // allocate from p[0..ss-1]
};

template<class T> struct My_alloc {
    Arena& a;
    My_alloc(Arena& aa) : a(aa) { }
    // usual allocator stuff
};

Arena my_arena1(new char[100000],100000);
Arena my_arena2(new char[1000000],1000000);

vector<int> v0; // allocate using default allocator

vector<int,My_alloc<int>> v1(My_alloc<int>{my_arena1}); // allocate from my_arena1

vector<int,My_alloc<int>> v2(My_alloc<int>{my_arena2}); // allocate from my_arena2

vector<int,Simple_alloc<int>> v3;   // allocate using Simple_alloc

Typically, the verbosity would be alleviated by the use of typedefs.

It is not guaranteed that the default allocator and Simple_alloc take up no space in a vector object, but a bit of elegant template metaprogramming in the library implementation can ensure that. So, using an allocator type imposes a space overhead only if its objects actually has state (like My_alloc).

A rather sneaky problem can occur when using containers and user-defined allocators: Should an element be in the same allocation area as its container? For example, if you use Your_alloc for Your_string to allocate its elements and someone else uses My_alloc to allocate elements of My_vector, then which allocator should be used for string elements in My_vector<Your_alloc>>? The solution is the ability to tell a container which allocator to pass to elements. For example, assuming that there is an allocator My_alloc and you want a vector<string> that uses My_alloc for both the vector element and string element allocations, first, you must make a version of string that accepts My_alloc objects:

using xstring = basic_string<char, char_traits<char>, My_alloc<char>>;  // a string with my allocator

Then, you must make a version of vector that accepts those strings, accepts a My_alloc object, and passes that object on to the string:

using svec = vector<xstring,scoped_allocator_adaptor<My_alloc<xstring>>>;   

Finally, we can make an allocator of type My_alloc<xstring>:

svec v(svec::allocator_type(My_alloc<xstring>{my_arena1}));

Now svec is a vector of strings using My_alloc to allocate memory for strings. What’s new is that the standard library “adaptor” (“wrapper type”) scoped_allocator_adaptor is used to indicate that string also should use My_alloc. Note that the adaptor can (trivially) convert My_alloc<xstring> to the My_alloc<char> that string needs.

So, we have four alternatives:

// vector and string use their own (the default) allocator:
using svec0 = vector<string>;
svec0 v0;

// vector (only) uses My_alloc and string uses its own (the default) allocator:
using svec1 = vector<string,My_alloc<string>>;
svec1 v1(My_alloc<string>{my_arena1});

// vector and string use My_alloc (as above):
using xstring = basic_string<char, char_traits<char>, My_alloc<char>>;
using svec2 = vector<xstring,scoped_allocator_adaptor<My_alloc<xstring>>>;
svec2 v2(scoped_allocator_adaptor<My_alloc<xstring>>{my_arena1});

// vector uses My_alloc and string uses My_string_alloc:
using xstring2 = basic_string<char, char_traits<char>, My_string_alloc<char>>;
using svec3 = vector<xstring2,scoped_allocator_adaptor<My_alloc<xstring>, My_string_alloc<char>>>;  
svec3 v3(scoped_allocator_adaptor<My_alloc<xstring2>, My_string_alloc<char>>{my_arena1,my_string_arena}); 

Obviously, the first variant, svec0, will be by far the most common, but for systems with serious memory-related performance constraints, the other versions (especially svec2) can be important. A few typedefs would make that code a bit more readable, but it is good it is not something you have to write every day. The scoped_allocator_adaptor2 is a variant of scoped_allocator_adaptor for the case where the two non-default allocators differ.

See also: