chunkedseq
container library for large in-memory data sets
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages
Customized data structures by cached measurement

This documentation covers essential concepts that are needed to implement custom data structures out of various instantiations of the chunkedseq structure. Just like the Finger Tree of Hinze and Patterson, the chunkedseq can be instantiated in certain ways to yield asymptotically efficient data structures, such as associative maps, priority queues, weighted sequences, interval trees, etc. A summary of these ideas that is presented in greater detail can be find in the original publication on finger trees and in a blog post.

In this tutorial, we present the key mechanism for building customized data structures: monoid-cached measurement. We show how to use monoid-cached measurements to implement a powerful form of split operation that affects chunkedseq containers. Using this split operation, we then show how to apply our cached measurement scheme to build two new data structures:

Taking measurements

Let \(S\) denote the type of the items contained by the chunkedseq container and \(T\) the type of the cached measurements. For example, suppose that we want to define a weighted chunkedseq container of std::strings for which the weights have type weight_type. Then we have: \(S = \mathtt{std::string}\) and \(T = \mathtt{weight\_type}\). How exactly are cached measurements obtained? The following two methods are the ones that are used by our C++ package.

Measuring items individually

A measure function is a function \(m\) that is provided by the client; the function takes a single item and returns a single measure value:

\[ \begin{array}{rcl} m(s) & : & S \rightarrow T \end{array} \]

Example: the "size" measure

Suppose we want to use our measurement to represent the number of items that are stored in the container. We call this measure the size measure. The measure of any individual item always equals one:

\[ \begin{array}{rclll} \mathtt{size}(s) & : & S \rightarrow \mathtt{long} & = & 1 \end{array} \]

Example: the "string-size" measure

The string-size measurement assigns to each item the weight equal to the number of characters in the given string.

\[ \begin{array}{rclll} \mathtt{string\_size}(str) & : & \mathtt{string} \rightarrow \mathtt{long} & = & str.\mathtt{size}() \end{array} \]

Measuring items in contiguous regions of memory

Sometimes it is convenient to have the ability to compute, all at once, the combined measure of a group of items that is referenced by a given "basic" segment. For this reason, we require that, in addition to \(m\), each measurement scheme provides a segment-wise measure operation, namely \(\vec{m}\), which takes the pair of pointer arguments \(begin\) and \(end\) which correspond to a basic segment, and returns a single measured value.

\[ \begin{array}{rcl} \vec{m}(begin, end) & : & (S^\mathtt{*}, S^\mathtt{*}) \rightarrow T \end{array} \]

The first and second arguments correspond to the range in memory defined by the segment \((begin, end]\). The value returned by \(\vec{m}(begin, end)\) should equal the sum of the values \(m(\mathtt{*}p)\) for each pointer \(p\) in the range \((begin, end]\).

Example: segmented version of our size measurement

This operation is simply \(\vec{m}(begin, end) = |end-begin|\), where our segment is defined by the sequence of items represented by the range of pointers \((begin, end]\).

The measure descriptor

The measure descriptor is the name that we give to the C++ class that describes a given measurement scheme. This interface exports deinitions of the following types:

Type Description
value_type type \(S\) of items stored in the container
measured_type type \(T\) of item-measure values

And this interface exports definitions of the following methods:

Members Description
measured_type operator()(const value_type& v) returns \(m(\mathtt{v})\)
measured_type operator()(const value_type* begin, const value_type* end) returns \(\vec{m}(\mathtt{begin}, \mathtt{end})\)

Example: trivial measurement

Our first kind of measurement is one that does nothing except make fresh values whose type is the same as the type of the second template argument of the class.

template <class Item, class Measured>
class trivial {
public:
using value_type = Item;
using measured_type = Measured;
measured_type operator()(const value_type& v) const {
return measured_type();
}
measured_type operator()(const value_type* lo, const value_type* hi) const {
return measured_type();
}
};

The trivial measurement is useful in situations where cached measurements are not needed by the client of the chunkedseq. Trivial measurements have the advantage of being (almost) zero overhead annotations.

Example: weight-one (uniformly sized) items

This kind of measurement is useful for maintaining fast access to the count of the number of items stored in the container.

template <class Item, class Measured, int Item_weight=1>
class uniform {
public:
using value_type = Item;
using measured_type = Measured;
const int item_weight = Item_weight;
measured_type operator()(const value_type& v) const {
return measured_type(item_weight);
}
measured_type operator()(const value_type* lo, const value_type* hi) const {
return measured_type(hi - lo);
}
};

Example: dynamically weighted items

This technique allows the client to supply to the internals of the chunkedseq container an arbitrary weight function. This client-supplied weight function is passed to the following class by the third template argument.

template <class Item, class Weight, class Client_weight_fct>
class weight {
public:
using value_type = Item;
using measured_type = Weight;
using client_weight_fct_type = Client_weight_fct;
private:
client_weight_fct_type client_weight_fct;
// for debugging purposes
bool initialized;
public:
weight() : initialized(false) { }
weight(const client_weight_fct_type& env)
: client_weight_fct(env), initialized(true) { }
measured_type operator()(const value_type& v) const {
return client_weight_fct(v);
}
measured_type operator()(const value_type* lo, const value_type* hi) const {
measured_type m = 0;
for (auto p = lo; p < hi; p++)
m += operator()(*p);
return m;
}
client_weight_fct_type get_env() const {
assert(initialized);
return client_weight_fct;
}
void set_env(client_weight_fct_type wf) {
client_weight_fct = wf;
initialized = true;
}
};

Example: combining cached measurements

Often it is useful to combine meaurements in various configurations. For this purpose, we define the measured pair, which is just a structure that has space for two values of two given measured types, namely Measured1 and Measured2.

template <class Measured1, class Measured2>
class measured_pair {
public:
Measured1 value1;
Measured2 value2;
measured_pair() { }
measured_pair(const Measured1& value1, const Measured2& value2)
: value1(value1), value2(value2) { }
};
template <class Measured1, class Measured2>
measured_pair<Measured1,Measured2> make_measured_pair(Measured1 m1, Measured2 m2) {
measured_pair<Measured1,Measured2> m(m1, m2);
return m;
}

The combiner measurement just combines the measurement strategies of two given measures by pairing measured values.

template <class Item, class Measure1, class Measure2>
class combiner {
public:
using measure1_type = Measure1;
using measure2_type = Measure2;
using value_type = Item;
using measured_type = measured_pair<measure1_type, measure2_type>;
measure1_type meas1;
measure2_type meas2;
combiner() { }
combiner(const measure1_type meas1)
: meas1(meas1) { }
combiner(const measure2_type meas2)
: meas2(meas2) { }
combiner(const measure1_type meas1, const measure2_type meas2)
: meas1(meas1), meas2(meas2) { }
measured_type operator()(const value_type& v) const {
return make_measured_pair(meas1(v), meas2(v));
}
measured_type operator()(const value_type* lo, const value_type* hi) const {
return make_measured_pair(meas1(lo, hi), meas2(lo, hi));
}
};

Using algebras to combine measurements

Recall that a monoid is an algebraic structure that consists of a set \(T\), an associative binary operation \(\oplus\) and an identity element \(\mathbf{I}\). That is, \((T, \oplus, \mathbf{I})\) is a monoid if:

Examples of monoids include the following:

A group is a closely related algebraic structure. Any monoid is also a group if the monoid has an inverse operation \(\ominus\):

The algebra descriptor

We require that the descriptor export a binding to the type of the measured values that are related by the algebra.

Type Description
value_type type of measured values \(T\) to be related by the algebra

We require that the descriptor export the following members. If has_inverse is false, then it should be safe to assume that the inverse(x) operation is never called.

Static members Description
const bool has_inverse true, iff the algebra is a group
value_type identity() returns \(\mathbf{I}\)
value_type combine(value_type x, value_type y) returns x \(\oplus\) y
value_type inverse(value_type x) returns \(\ominus\) x

Example: trivial algebra

The trivial algebra does nothing except construct new identity elements.

class trivial {
public:
using value_type = struct { };
static constexpr bool has_inverse = true;
static value_type identity() {
return value_type();
}
static value_type combine(value_type, value_type) {
return identity();
}
static value_type inverse(value_type) {
return identity();
}
};

Example: algebra for integers

The algebra that we use for integers is a group in which the identity element is zero, the plus operator is integer addition, and the minus operator is integer negation.

template <class Int>
class int_group_under_addition_and_negation {
public:
using value_type = Int;
static constexpr bool has_inverse = true;
static value_type identity() {
return value_type(0);
}
static value_type combine(value_type x, value_type y) {
return x + y;
}
static value_type inverse(value_type x) {
return value_type(-1) * x;
}
};

Example: combining algebras

Just like with the measurement descriptor, an algebra descriptor can be created by combining two given algebra descriptors pairwise.

template <class Algebra1, class Algebra2>
class combiner {
public:
using algebra1_type = Algebra1;
using algebra2_type = Algebra2;
using value1_type = typename Algebra1::value_type;
using value2_type = typename Algebra2::value_type;
using value_type = measure::measured_pair<value1_type, value2_type>;
static constexpr bool has_inverse =
algebra1_type::has_inverse
&& algebra2_type::has_inverse;
static value_type identity() {
return measure::make_measured_pair(algebra1_type::identity(),
algebra2_type::identity());
}
static value_type combine(value_type x, value_type y) {
return measure::make_measured_pair(algebra1_type::combine(x.value1, y.value1),
algebra2_type::combine(x.value2, y.value2));
}
static value_type inverse(value_type x) {
return measure::make_measured_pair(algebra1_type::inverse(x.value1),
algebra2_type::inverse(x.value2));
}
};

Scans

A scan is an iterated reduction that maps to each item \(v_i\) in a given sequences of items \(S = [v_1, v_2, \ldots, v_n]\) a single measured value \(c_i = \mathbf{I} \oplus m(v_1) \oplus m(v_2) \oplus \ldots \oplus m(v_i)\), where \(m(v)\) is a given measure function. For example, consider the "size" (i.e., weight-one) scan, which is specified by the use of a particular measure function: \(m(v) = 1\). Observe that the size scan gives the positions of the items in the sequence, thereby enabling us later on to index and to split the chunkedseq at a given position.

For convenience, we define scan formally as follows. The operator returns the combined measured values of the items in the range of positions \([i, j)\) in the given sequence \(s\).

\[ \begin{array}{rclr} M_{i,j} & : & \mathtt{Sequence}(S) \rightarrow T \\ M_{i,i}(s) & = & \mathbf{I} & \\ M_{i,j}(s) & = & \oplus_{k = i}^j m(s_k) & \mathrm{if} \, i < j \end{array} \]

Why associativity is necessary

The cached value of an internal tree node \(k\) in the chunkedseq structure is computed by \(M_{i,j}(s)\), where \(s = [v_i, \ldots, v_j]\) represents a subsequence of values contained in the chunks of the subtree below node \(k\). When this reduction is performed by the internal operations of the chunkedseq, this expression is broken up into a set of subexpressions, for example: \(((m(v_i) \oplus m(v_{i+1})) \oplus (m(v_{i+2}) \oplus m(v_{i+3}) \oplus (m(v_{i+4}) \oplus m(v_{i+5}))) ... \oplus m(v_j))\). The partitioning into subexpressions and the order in which the subexpressions are combined depends on the particular shape of the underlying chunkedseq. Moreover, the particular shape is determined uniquely by the history of update operations that created the finger tree. As such, we could build two chunkedseqs by, for example, using different sequences of push and pop operations and end up with two different chunkedseq structures that represent the same sequence of items. Even though the two chunkedseqs represent the same sequence, the cached measurements of the two chunkedseqs are combined up to the root of the chunkedseq by two different partitionings of combining operations. However, if \(\oplus\) is associative, it does not matter: regardless of how the expression are broken up, the cached measurement at the root of the chunkedseq is guaranteed to be the same for any two chunkedseqs that represent the same sequence of items. Commutativity is not necessary, however, because the ordering of the items of the sequence is respected by the combining operations performed by the chunkedseq.

Why the inverse operation can improve performance

Suppose we have a cached measurement \(C = M_{i,j}(s)\) , where \(s = [v_i, \ldots, v_j]\) represents a subsequence of values contained in the same chunk somewhere inside our chunkedseq structure. Now, suppose that we wish to remove the first item from our sequence of measurements, namely \(v_i\). On the one hand, without an inverse operation, and assuming that we have not cached partial sums of \(C\), the only way to compute the new cached value is to recompute \((m(v_{i+1}) \oplus ... \oplus m(v_j))\). On the other hand, if the inverse operation is cheap, it may be much more efficient to instead compute \(\ominus m(v_i) \oplus C\).

Therefore, it should be clear that using the inverse operation can greatly improve efficiency in situations where the combined cached measurement of a group of items needs to be recomputed on a regular basis. For example, the same situation is triggered by the pop operations of the chunks stored inside the chunkedseq structure. On the one hand, by using inverse, each pop operation requires only a few additional operations to reset the cached measured value of the chunk. On the other, if inverse is not available, each pop operation requires recomputing the combined measure of the chunk, which although constant time, takes time proportion with the chunk size, which can be a fairly large fixed constant, such as 512 items. As such, internally, the chunkedseq operations use inverse operations whenever permitted by the algebra (i.e., when the algebra is identified as a group) but otherwise fall back to the most general strategy when the algebra is just a monoid.

Defining custom cached-measurement policies

The cached-measurement policy binds both the measurement scheme and the algebra for a given instantiation of chunkedseq. For example, the following are cached-measurement policies:

Remarks
To save space, the chunkedseq structure can be instantiated with the nullary cached measurement alone. No space is taken by the cached measurements in this configuration because the nullary measurement takes zero bytes. However, the only operations supported in this configuration are push, pop, and concatenate. The size cached measurement is required by the indexing and split operations. The various instantiations of chunkedseq, namely deque, stack and bag all use the size measure for exactly this reason.

The cached-measurement descriptor

The interface exports four key components: the type of the items in the container, the type of the measured values, the measure function to gather the measurements, and the algebra to combine measured values.

Type Description
measure_type type of the measure descriptor
algebra_type type of the algebra descriptor
value_type type \(S\) of items to be stored in the container
measured_type type \(T\) of measured values
size_type size_t

The only additional function that is required by the policy is a swap operation.

Static members Description
void swap(measured_type& x, measured_type& y) exchanges the values of x and y

Example: trivial cached measurement

This trivial cached measurement is, by itself, completely inert: no computation is required to maintain cached values and only a minimum of space is required to store cached measurements on internal tree nodes of the chunkedseq.

template <class Item, class Size>
class trivial {
public:
using size_type = Size;
using value_type = Item;
using algebra_type = algebra::trivial;
using measured_type = typename algebra_type::value_type;
using measure_type = measure::trivial<value_type, measured_type>;
static void swap(measured_type& x, measured_type& y) {
// nothing to do
}
};

Example: weight-one (uniformly sized) items

In our implementation, we use this cached measurement policy to maintain the size information of the container. The size() methods of the different chunkedseq containers obtain the size information by referencing values cached inside the tree by this policy.

template <class Item, class Size>
class size {
public:
using size_type = Size;
using value_type = Item;
using algebra_type = algebra::int_group_under_addition_and_negation<size_type>;
using measured_type = typename algebra_type::value_type;
using measure_type = measure::uniform<value_type, measured_type>;
static void swap(measured_type& x, measured_type& y) {
std::swap(x, y);
}
};

Example: weighted items

Arbitrary weights can be maintained using a slight generalization of the size measurement above.

template <class Item, class Weight, class Size, class Measure_environment>
class weight {
public:
using size_type = Size;
using value_type = Item;
using algebra_type = algebra::int_group_under_addition_and_negation<Weight>;
using measured_type = typename algebra_type::value_type; // = Weight
using measure_env_type = Measure_environment;
using measure_type = measure::weight<value_type, measured_type, measure_env_type>;
static void swap(measured_type& x, measured_type& y) {
std::swap(x, y);
}
};

Example: combining cached measurements

Using the same combiner pattern we alredy presented for measures and algebras, we can use the following template class to build combinations of any two given cached-measurement policies.

template <class Cache1, class Cache2>
class combiner {
public:
using algebra1_type = typename Cache1::algebra_type;
using algebra2_type = typename Cache2::algebra_type;
using measure1_type = typename Cache1::measure_type;
using measure2_type = typename Cache2::measure_type;
using size_type = typename Cache1::size_type;
using value_type = typename Cache1::value_type;
using algebra_type = algebra::combiner<algebra1_type, algebra2_type>;
using measured_type = typename algebra_type::value_type;
using measure_type = measure::combiner<value_type, measure1_type, measure2_type>;
static void swap(measured_type& x, measured_type& y) {
Cache1::swap(x.value1, y.value1);
Cache2::swap(x.value2, y.value2);
}
};

Splitting by predicate functions

Logically, the split operation on a chunkedseq container divides the underlying sequence into two pieces, leaving the first piece in the container targeted by the split and moving the other piece to another given container. The position at which the split occurs is determined by a search process that is guided by a predicate function. What carries out the search process? That job is the job of the internals of the chunkedseq class; the client is responsible only to provide the predicate function that is used by the search process. Formally, a predicate function is simply a function \(p\) which takes a measured value and returns either true or false.

\[ \begin{array}{rcl} p(m) & : & T \rightarrow \mathtt{bool} \end{array} \]

The search process guarantees that the position at which the split occurs is the position \(i\) in the target sequence, \(s = [v_1, \ldots, v_i, \ldots v_n]\), at which the value returned by \(p(M_{0,i}(s))\) first switches from false to true. The first part of the split equals \([v_1, \ldots, v_{i-1}]\) and the second \([v_i, \ldots, v_n]\).

The predicate function descriptor

In our C++ package, we represent predicate functions as classes which export the following public method.

Members Description
bool operator()(measured_type m) returns \(p(\mathtt{m})\)

Example: weighted splits

Let us first consider the small example which is given already for the weighted container. The action performed by the example program is to divide a given sequence of strings so that the first piece of the split contains approximately half of the even-length strings and the second piece the second half. In our example code (see the page linked above), we assign to each item a certain weight, which is defined formally as follows: if the length of the given string is an even number, return a 1; else, return a 0.

\[ \begin{array}{rcccl} m(str) & : & \mathtt{string} \rightarrow \mathtt{int} & = & \left\{ \begin{array}{l l} 1 & \quad \mathrm{if}\, str.\mathtt{size()}\, \mathrm{is\, an\, even\, number}\\ 0 & \quad \mathrm{otherwise} \end{array} \right. \end{array} \]

Let \(n\) denote the number of even-length strings in our source sequence. Then, the following predicate function delivers the exact split that we want.

\[ \begin{array}{rcccl} p(m) & : & int \rightarrow \mathtt{bool} & = & m \geq n/2 \end{array} \]

Let \(s\) denote the sequence of strings (i.e., ["Let's", "divide", "this", "string", "into", "two", "pieces"] that we want to split. The following table shows the logical states of the split process.

\(i\) 0 1 2 3 4 5 6
\(v_i\) Let's divide this string into two pieces
\(m(v_i)\) 0 1 1 1 1 0 1
\(M_{0,i}(s)\) 0 1 2 3 4 4 5
\(p(M_{0,i}(s))\) false false false true true true true
Remarks
Even though the search process might look like a linear search, the process in fact takes just logarithmic time in the number of items in the sequence. The logarithmic time bound is possible thanks to the fact that internal nodes of the chunkedseq tree (which is itself a tree whose height is logarithmic in the number of items) are annotated by partial sums of weights.

Example: using cached measurement to implement associative maps

Our final example combines all of the elements of cached measurement to yield an asymptotically efficient implementation of associative maps. The idea behind the implementation is to represent the map internally by a chunkedseq container of key-value pairs. The key to efficiency is that the items in the chunkedseq are stored in descending order. When key-value pairs are logically added to the map, the key-value pair is physically added to a position in the underlying sequence so that the descending order is maintained. The insertion and removal of key-value pairs is achieved by splitting by a certain predicate function which we will see later. At a high level, what is happening is a kind of binary search that navigates the underlying chunkedseq structure, guided by carefully chosen cached key values that annotate the interior nodes of the chunkedseq.

Remarks
We could have just as well maintain keys in ascending order.

Optional values

Our implementation uses optional values, which are values that logically either contain a value of a given type or contain nothing at all. The concept is similar to that of the null pointer, except that the optional value applies to any given type, not just pointers.

template <class Item, class Item_swap>
class option {
public:
using self_type = option<Item, Item_swap>;
Item item;
bool no_item;
option()
: item(), no_item(true) { }
option(const Item& item)
: item(item), no_item(false) { }
option(const option& other)
: item(other.item), no_item(other.no_item) { }
void swap(option& other) {
Item_swap::swap(item, other.item);
std::swap(no_item, other.no_item);
}
bool operator<(const self_type& y) const {
if (no_item && y.no_item)
return false;
if (no_item)
return true;
if (y.no_item)
return false;
return item < y.item;
}
bool operator>=(const self_type& y) const {
return ! (*this < y);
}
};

Observe that our class implements the "less-than" operator: <. Our implementation of this operator lifts any implementation of the same operator at the type Item to the space of our option<Item>: that is, our operator treats the empty (i.e., nullary) optional value as the smallest optional value. Otherwise, our the comparison used by our operator is the implementation already defined for the given type, Item, if such an implementation is available.

The measure descriptor

The type of value returned by the measure function (i.e., measured_type) is the optional key value, that is, a value of type option<key_type>. The measure function simply extracts the smallest key value from the key-value pairs that it has at hand and packages them as an optional key value.

template <class Item, class Measured>
class get_key_of_last_item {
public:
using value_type = Item;
using key_type = typename value_type::first_type;
using measured_type = Measured;
measured_type operator()(const value_type& v) const {
key_type key = v.first;
return measured_type(key);
}
measured_type operator()(const value_type* lo, const value_type* hi) const {
if (hi - lo == 0)
return measured_type();
const value_type* last = hi - 1;
key_type key = last->first;
return measured_type(key);
}
};

The monoid descriptor

The monoid uses for its identity element the nullary optional key. The combining operator takes two optional key values and of the two returns either the smallest one or the nullary optional key value.

template <class Option>
class take_right_if_nonempty {
public:
using value_type = Option;
static constexpr bool has_inverse = false;
static value_type identity() {
return value_type();
}
static value_type combine(value_type left, value_type right) {
if (right.no_item)
return left;
return right;
}
static value_type inverse(value_type x) {
util::atomic::fatal([] {
std::cout << "inverse operation not supported" << std::endl;
});
return identity();
}
};

The descriptor of the cached measurement policy

The cache measurement policy combines the measurement and monoid descriptors in a straightforward fashion.

template <class Item, class Size, class Key_swap>
class map_cache {
public:
using size_type = Size;
using value_type = Item;
using key_type = typename value_type::first_type;
using option_type = option<key_type, Key_swap>;
using algebra_type = take_right_if_nonempty<option_type>;
using measured_type = typename algebra_type::value_type; // = option_type
using measure_type = get_key_of_last_item<value_type, measured_type>;
static void swap(measured_type& x, measured_type& y) {
x.swap(y);
}
};

The associative map

The associative map class maintains the underlying sorted sequence of key-value pairs in the field called seq. The method called upper is the method that is employed by the class to maintain the invariant on the descending order of the keys. This method returns either the position of the first key that is greater than the given key, or the position of one past the end of the sequence if the given key is the greatest key.

As is typical of STL style, the indexing operator is used by the structure to handle both insertions and lookups. The operator works by first searching in its underlying sequence for the key referenced by its parameter; if found, the operator updates the value component of the corresponding key-value pair. Otherwise, the operator creates a new position in the sequence to put the given key by calling the insert method of seq at the appropriate position.

template <class Item>
class std_swap {
public:
static void swap(Item& x, Item& y) {
std::swap(x, y);
}
};
template <class Key,
class Item,
class Compare = std::less<Key>,
class Key_swap = std_swap<Key>,
class Alloc = std::allocator<std::pair<const Key, Item> >,
int chunk_capacity = 8
>
class map {
public:
using key_type = Key;
using mapped_type = Item;
using value_type = std::pair<key_type, mapped_type>;
using key_compare = Compare;
using allocator_type = Alloc;
using reference = value_type&;
using const_reference = const value_type&;
using pointer = value_type*;
using const_pointer = const value_type*;
using difference_type = ptrdiff_t;
using size_type = size_t;
using key_swap_type = Key_swap;
private:
using cache_type = map_cache<value_type, size_type, key_swap_type>;
using container_type = chunkedseq::bootstrapped::deque<value_type, chunk_capacity, cache_type>;
using option_type = typename cache_type::measured_type;
public:
using iterator = typename container_type::iterator;
private:
// invariant: items in seq are sorted in ascending order by their key values
mutable container_type seq;
mutable iterator it;
iterator upper(const key_type& k) const {
option_type target(k);
it.search_by([target] (const option_type& key) {
return target >= key;
});
return it;
}
public:
map() {
it = seq.begin();
}
map(const map& other)
: seq(other.seq) {
it = seq.begin();
}
size_type size() const {
return seq.size();
}
bool empty() const {
return size() == 0;
}
iterator find(const key_type& k) const {
iterator it = upper(k);
return ((*it).first == k) ? it : seq.end();
}
mapped_type& operator[](const key_type& k) const {
it = upper(k);
if (it == seq.end()) {
// key k is larger than any key current in seq
val.first = k;
seq.push_back(val);
it = seq.end()-1;
} else if ((*it).first != k) {
// iterator it points to the first key that is less than k
val.first = k;
it = seq.insert(it, val);
}
return (*it).second;
}
void erase(iterator it) {
if (it == seq.end())
return;
if (it == seq.end()-1) {
seq.pop_front();
return;
}
seq.erase(it, it+1);
}
size_type erase(const key_type& k) {
size_type nb = seq.size();
erase(find(k));
return nb - seq.size();
}
std::ostream& stream(std::ostream& out) const {
out << "[";
size_type sz = size();
seq.for_each([&] (value_type v) {
out << "(" << v.first << "," << v.second << ")";
if (sz-- != 1)
out << ",";
});
return out << "]";
}
iterator begin() const {
return seq.begin();
}
iterator end() const {
return seq.end();
}
void check() const {
seq.check();
}
};