I'm using a doubly linked list container I've written as the working example. The textbook I'm using is from 2001, so feel free to point out where later versions of the C++ standard should be used instead.

Notes:

List::iterator functionality mimics pointer arithmetic.

Unlike most containers, list beginning is marked by a specific element which holds no data, to make reverse iteration easier:
[begins][data][data][data][data][data][ends]

2 Answers
2

Comment on notes:

List::iterator functionality mimics pointer arithmetic.

If you call it an iterator then it really should.

But you should be careful. There are actually several classes of iterator. The pointer implements the Random Access Iterator. An iterator for a doubly linked list usually only implements Reversible Iterator. The difference is that Random Access Iterator can do i += 10 in O(1) while the reversible iterator does not need this functionality.

Unlike most containers, list beginning is marked by a specific element which holds no data, to make reverse iteration easier: [begins][data][data][data][data][data][ends]

Actually using sentinels (A special marker in the list) is a very common way of implementing a doubly linked list (it makes the implementation much easier because you don't need to worry about the empty list and NULL).

But just because you have a sentinel value internally you do not need to expose that to the user of your class (that is not common). Usually begin() would return the first value after the sentinel.

That's a strange behavior. Also it makes swapping your container with other peoples containers bothersome (as you have a change in expected behavior). You should definitely conform to the expected behavior (unless you have a very specific use case that only your class is good for and you document it thoroughly).

The List container plays more elegantly with basic loops...see main.

We will see about that.

A.... no it does not.
Because it does not do what people expect. If you stray from expected behavior then you confuse the maintainer. It may look very neat to you. But to experienced programmers it looks like you fucked up. Then we have to go and dig into the implementation to verify that it actually works as expected (thus wasting time for the maintainer which is probably not you (but does have an axe and your address)).

NULL is supposed to be used for pointers only. It marks a pointer that points at nothing. The last three elements in your object are not pointers and thus assigning NULL makes no sense. It compiles because in C++03 NULL was usually the integer zero. In this case the NULL is being converted back to zero and assigned to these values. You will not be able to tell the difference between NULL and zero. In C++11 the nullptr was introduced. This is not convertible back to zero and thus will generate a compiler error when used above.

Also your type T may not be constructable with a NULL pointer (or zero). So this will generate compilation errors.

I would have done:

template <class T>
struct element {
element<T> *prev = nullptr;
element<T> *next = nullptr;
T data;
int elem_ID = 0;
char t_flag = '\0';
element(T const& copy)
: data(copy)
{}
// In C++11 we introduced the concept of moving data
// rather than copying it (as in C++03). This is as
// quick as copying and if the structure T is complicated
// then much faster. If the object T does not know how to
// move itself then it will use the copy above so it is
// also backwards compatible with old code.
element(T&& move)
: data(std::forward<T>(move))
{}
};

struct List

Looks good at first glance. You need a couple of interface changes to make it standards compliant.

One of the major important things in C++ (over C) is const correctness. It is very often that you see objects being passed around as const& (const references). This means you can read but not alter them. But it also means that your class must make available ways for your object to be read (via iterators). As a result I would also expect the following.

Notice the extra const on the end of the function name. This is an indication that this call will not modify the object so the compiler will allow this method to be called when the object is const (ie actually const or being accessed via a const reference).

The iterator returned from these calls should also be slightly different from normal iterators in that they can read from the container but will not allow modification of the container.

Third thing to note is the use of void. This is uncommon in C++ (it means the same but is rarely used (best to not use it but not going to make any difference if you do)). Note this is different from the C meaning of void as a parameter.

Sentential

Your use of sentenals is fine for simple types of T. But will not work for anything with a complex constructor. You should have a different object that does not have the T data load for your sentential.

Also most implementations only use a single sentential object. The pointers of the sentinel wrap around to point at itself. This way begin always points to the first element (or the sentinel if it is empty). While end always points at the sentinel. Then sentinel->next is the first element while sentinal->last is the last element (which could be itself if there are no elements).

\$\begingroup\$"Your use of sentinels is fine for simple types of T. But will not work for anything with a complex constructor." Can you explain what you mean by this? ---------------- "You should have a different object that does not have the T data load for your sentential." Yes, it makes sense to have a separate & simpler object(Sentry), but how to resolve the issue of the next/prev potentially being a Sentry, what to use instead of: element<T>*\$\endgroup\$
– tukJul 28 '15 at 13:08

Here are some things that may help you improve your program. One thing you could do for yourself that would greatly improve your programming is to get a newer book. The C++ language has changed considerably since 2001, and there's not much reason to learn an obsolete version and bad habits.

Don't over-specify member functions

When you declare functions inline, as in your elem_iter structure, it is not necessary to repeat that each inline function is member of elem_iter. So instead of this:

T elem_iter::operator*(void){

write this:

T operator*(void){

Supply the missing postfix operators

There is a difference between iter++ and ++iter. Your code defines only the latter version, but attempts to use the former.

Use nullptr rather than NULL

Modern C++ uses nullptr rather than NULL. See this answer for why and how it's useful. Once you do that, you'll see that you have been abusing NULL and using it as both an integer and a character value instead of just a pointer. For example, in List::clear() you have this:

begins->data = NULL;
ends->elem_ID = NULL;

However, data is defined as class T and elem_ID is defined as an int. Instead, assign 0 to elem_ID and don't do anything with data.

Improve your constructors

The List class has three data elements; begins, ends and element_count. The constructor should create and initialize those three things. A more modern style for your constructor might be this:

Don't #include headers that aren't needed

This code includes <vector> and <list> but neither are actually used. Those lines should be deleted.

Use a consistent naming convention

Pick a naming convention and use it consistently. In this code, some of the classes are lowercase, such as elem_iter but List is capitalized. One common convention is the use uppercase for all classes and structures.

Keep class data and functions separated

The elem_iter class has a single data element target but it's hard to spot because it's hiding amid a number of functions. Better would be to gather all data items together and put them at either the beginning or the end of the class.

Prefer to keep data members private

It's generally bad practice to allow other code to reach inside objects and grab or alter data. For that reason, all of your structs should be classes and only some of the member functions should be public. Where you have closely related classes, you can declare them as friend to allow access to private members. For example, the code for your element might look like this:

In addition to const to indicate that the argument is not changed by the function, we also have a const at the end to indicate that the operator doesn't change the object. Also, this->target is redundant and return is not a function call and therefore does not need parentheses.

Consider simplifying the interface

It's not clear to me that the elem_ID field is actually useful, so if I were writing this, I think I would omit it and all of the functions that rely on it. It's your choice of course, but often a simpler interface makes for an easier-to-use class.

Consider non-numeric objects

Right now, various places in the code limit class T to be numeric or at least something that can be initialized to 0. By some judicious rewriting, this artificial restriction can be eliminated. For instance, create a List of std::string and you will see where I mean.

Omit return 0

When a C++ program reaches the end of main the compiler will automatically generate code to return 0, so there is no reason to put return 0; explicitly at the end of main.