Friday, 28 November 2008

In this part, we'll get back to some basics. In a brief intermission from iterators, we'll add the complement to front(), back(), which returns a reference to the last item in the buffer. We provide a const and a non-const version:

Part 10 already! There's quite a lot of detail involved in writing an STL-like container. Now we're going to delve into some of the more interesting stuff...

I promised you iterators, and so iterators is what you'll get. Like the indexing operations we added in part 8, a circular buffer traditionally doesn't let you iterate through the contents. But where's the fun in that?

Different containers support different classes of iterator:

An input iterator allows you to read values with forward movement. It can be incremented, compared, and deferenced.

An output iterator also moves forwards, but allows you to write values rather than read them on the way.

A forward iterator effectively combines the input and output iterators, allowing you to read or write values with forward movement.

A bidirectional iterator adds the capability to move backwards; you can increment and decrement them.

A random access iterator allows you to read and write with the ability to do pointer arithmentic and pointer-like comparisons of the iterators.

A reverse iterator is a bidirectional or random iterator that moves through the container in the reverse direction.

So what kind of iterator should we support? In our simple container it's perfectly possible to support a random access iterator.

First steps

The container must supply the following nested types: iterator and const_iterator. These can be typedefs or nested class declarations (for example, the most efficient iterator implementation for the std::vector is a typedef of a pointer). However, to start off with, we can simply construct a nested class inside circular_buffer.

Let's start off with the non-const iterator version.

// In declarationclass iterator; iterator begin(); iterator end();

Our most implementation is not quite as simple as a pointer, but not much more complex. We need only maintain an index into the buffer and a back-pointer to the parent circular_buffer class. The iterator can be implemented in terms of circular_buffer::operator[].

Like a container, and an allocator, an STL iterator must provide a number of nested type definitions for algorithms to be able to use them appropriately. These include:

difference_type

value_type

pointer

reference

iterator_category

These are picked up by the std::iterator_traits class (see section 24.3.1 of the standard). You needn't follow the mechanics here completely, since the standard library provides a convenient iterator base class, std::iterator, that your iterator implementation can inherit from. This base class will define all the relevant types for you based on the template parameters you give it. The template parameters are:

the iterator category (these are tag types in the std namespace)

the difference_type

the pointer type

the reference type

All of these can be selected from circular_buffer's existing pool of typedefs.

This works nicely enough, although we'll refine the design as we add more facilities to the iterator.

Make sure that you understand the difference between preincrement and postincrement and how you overload each operator in a class declaration. Also make sure you know the difference between operator* and operator->.

Implementing reverse iterators

If we want to support reverse iteration, we must provide type definitions for the iterator and const_iterator counterparts: reverse_iterator and const_reverse_iterator.

You may recall that my company has decided to shut down our Cambridge office and scale back its development operation significantly. Since that announcement I've been busily looking for other gainful employment, and have been pleasantly surprised that the job market is not entirely stagnant. I've had a number interesting leads that I'm still following up.

However, in a dramatic turnaround I have now secured a safe position in my existing organisation, which is a huge weight off my mind. (That doesn't necessarily mean I'll stop looking for other work, but it means I'm secure in the meantime). I am The Last Man Standing.

I was a little disappointed that we didn't have an Apprentice-style face-off for the position, with the CEO saying you're fired to team members in turn. In the end the selection process was little more than a job proposal, some phone calls to the states, and a little negotiation. No pistols at dawn.

This new role is going to be somewhat taxing. Solely responsible for the future of the codebase we've developed, reporting to the US, without the safety net of on-site QA or hardware support, working on my own (in a location yet to be determined). I'm genuinely unsure of how well I'll be able to work on my own; I've never done that before. It's going to be a bumpy ride...

And, I'm not entirely sure how I'm going to structure my development process. Let's be honest, it's hard to do XP in a team of one. Pair programming will be somewhat tricky, and code reviews hard to elicit. I'm going to need to build in something to replace the accountability in our existing development process. All comments and suggestions welcomed...

It's been a great three years working with a team of talented and motivated people, and a real shame to get to the end of the era at this early stage. There was a lot more potential in the team that we hadn't yet exploited.

Wednesday, 26 November 2008

Here's a little extra method we can easily add: back(). In a similar way to std::vector, std::deque, and std::list, the circular_buffer can support a back method, which returns a reference to the item at the end of the container.

Remember, m_back refers to the next unused item, so if we have anything in the buffer, it will be stored in the previous element.

The circular_buffer is looking in pretty good shape now. We've squashed bugs and introduced an allocator. And, to be fair, we could stop here and have a perfectly usable class.

However, let's add some more methods to the container to learn a bit more about writing STL-like containers. The story can't end here after all, there's been no dramatic climax.

operator[] and at

These two methods aren't strictly required of a circular buffer. Section 23.1.1 of the standard claims these operations are only provided when they take constant time. We can achieve constant time indexing with our class, so let's write them.

at() is an index operator that performs bounds checking, throwing a std::out_of_range error if the index is invalid. operator[] itself need perform no bounds checking. Given our wrap() method, the implementations are simple:

About Me

Pete Goodliffe is a software developer, columnist, speaker, and author who never stays at the same place in the software food chain; he's worked in numerous languages on diverse projects.
He also has extensive experience in teaching and mentoring programmers, and writes the regular "Professionalism in Programming" column for ACCU's C Vu magazine (www.accu.org).
Pete's popular book, Code Craft, is a practical and entertaining investigation of the entire programming persuit. In about 600 pages. No mean feat! Pete enjoys writing excellent, bug-free code, so he can spend more time having fun with his kids. He has a passion for curry and doesn't wear shoes.