Practical use of the Standard C++ and Boost libraries
by Yannick Loiti
ère (ycl2r@virginia.edu)

The C++ library is entirely1 composed of interlocking, extensible components. While the standard containers are probably the most often used, more flexible and (arguably) clearer code can be written by combining them judiciously.

Preliminary notes

The STL implementation in MS Visual C++ 6.0 is particularly flaky. Replacements for it exist but, unfortunately, the compiler itself is so far from standard-compliance that many of the more advanced methods presented here fail pathetically. MSVC 7.0  (a.k.a. .NET) and later (7.1) handle these cases much better, as does g++ 3.0 or better (2.95 has some library issues).

All the standard classes and algorithms are implemented in the ::std namespace. The ::std:: scoping prefix has been omitted for convenience, as if a using namespace std; statement had been written. Likewise with the boost:: prefix.

Make sure you read the footnotes !

Easy C++ Formatting - http://www.boost.org/libs/format/index.htm

Formatted IO, especially of multiple fields is, let's admit it, tedious in C++. You have to chain often a large number of << just to insert a single space. On the other hand, C has printf, which lets you define a format string an then pass the values that will replace the placeholders. It is relatively easier to use, but only works for basic types, and is horribly type-unsafe. Wait no more, boost::format fixes all that.

Boost::format works very similarly to printf, taking a printf-style format string and then a number of arguments :

printf( "%d %f %s", 10, 20.5f, "foo" );
cout << boost::format fmt( "%d %f %s" ) % 10 % 20.5f % "foo";

However, it also presents a number of differences with printf :

Sample code :

boost::format fmt("writing %1%,  x=%2% : %3%-th try");
fmt % "toto" % 40.23 % 50;
cout << fmt << endl;
// prints "writing toto, x=40.230 : 50-th try"
string str = fmt.str();
// assigns "writing toto, x=40.230 : 50-th try" to str
cout << fmt % 100 % Vec( 10, 10, 10 ) % "foo" << endl;
// prints "writing 100, x=(10, 10, 10) : foo-th try"
// Vec is a user-defined class overloading operator<<

Containers

Even when an existing function needs a C array, you can use a C++ std::vector (only), and pass a pointer to the first element (obtained with &vec[0] and not vec, vectors are objects, not arrays), to the function.

struct vertex { float x,y,z };
vector<vertex> vertices;  // Then and the vertices

glVertexPointer( 3, GL_FLOAT, 0, &vertices[0] );
glDrawArrays( GL_TRIANGLES, 0, vertices.size() );

Iterators

Iteration over a container's elements is a recurring task in many programs. Naïve container implementations often provide special functions to move from one element to the other. The C++ library gives pointer-like (at least dereference, increment and inequality) access to container elements for all of its containers by providing iterators2.

There are two ways of accessing a C array's elements. Either through an index notation a[i], or by using a pointer *(a+i). Both ways can be used to iterate through the array.

const size_t face_count = 1000;
Face model[face_count];  // Then add the faces
size_t index; for( index = 0; index != face_count; ++index ) Face[i].Draw(); Face* itor;
for( itor = array; itor != model + face_count; ++itor ) itor->Draw();

The C++ library allows syntax similar to the second case above for all of its containers.

list<Face> model;  // Then add the faces
list<Face>::iterator itor;
for( itor = model.begin(); itor != model.end(); ++itor )
     itor->Draw();

With C arrays, the loop is over when it reaches one-past the last element of the array ( index==facecount or itor == model + face_count ). Likewise, iterator ranges are semi-open : container::begin() 'points' to the first element, container::end() 'points' one-past the last element, and we check non-equality with it3. As a consequence, there are always container::end() - container::begin() elements in the container4 – if they are equal, the container is empty and the loop is never executed.

 

Iterator operations and Categories (see handout on containers)

Category

Output

Input

Forward

Bidirectional

Random

Read


=*p
=*p
=*p
=*p

Access


->
->
->
-> []

Write

*p=


*p=
*p=
*p=

Iteration

++
++
++
++ --
++ -- + - += -=

Comparison


== !=
== !=
== !=
== != < > <= >=



Algorithms

The simplest example of algorithm simply calls a unary function for each element of the container. It is, unsurprisingly, named for_each.

for_each( begin, end, func ); 

is equivalent to

for( something itor = begin; itor != end; ++itor ) func(*itor );

Therefore, for our Model-as-list-of-faces :

for_each( model.begin(), model.end(), Clip );

calls Clip( f ) for each face f in model. The fact that it is a linked-list never arose. It could have been a vector, a C array, or a user-defined data structure with the proper interface.

Another algorithm that is used in Assignment 2 is transform. It correspond to the application of a unary or binary function to a range of elements, storing the result in another range. Thus, for the binary function version :

transform( arg1_begin, arg1_end, arg2_begin, dest_begin, func );

is roughly equivalent to

something arg1, arg2, dest;
for( arg1 = arg1_begin, arg2 = arg2_begin, dest = dest_begin; 
     arg1 != arg1_end; ++arg1, ++arg2, ++dest ) 
     *dest = func( *arg1, *arg2 );

And the code

vector<bool> isect;  // then initialise isect
transform( model.begin(), model.end(), isect.begin(), isect.begin(), Intersect );

applies the Intersect function onto Face/bool tuples, assigning the result back to isect.

I know which version I prefer, especially when it comes to the declaration of itor, arg1, arg2 and dest. When the loop is manually written out, something must actually be declared as an iterator on the container we are working with. If the container is a list<Face>, you have to make it a list<Face>::iterator5; if it is a C array of double, you have to make it a double*6. Since STL algorithms are templated functions, they can silently absorb the return values of begin() and end(), and you never have to see the actual loop variables.

There are many more algorithms performing more or less complicated operations. Look them up.

Function objects - http://www.boost.org/libs/libraries.htm#Function-objects

STL algoriths are quite handy, but their interface still appears rather rigid. They do accept a function parameter that modify their behavior, but the expected function signature is well defined as well : unary or binary functions with arguments and return values 'of the proper type'. However, as mentioned above, STL algorithms are templated functions. Which means that they do not demand as specific type7, but rather that a number of operations on these types be syntactically correct.

Namely, that 'function' parameter must be usable in a function-like manner : f(), f(a), f(a,b) ...

Functions and function pointers8 are the obvious candidates for such syntax, but any object which define the function call operator operator()( arg1, arg2 ...) is also valid. Which, among other things, excludes pointers to member functions, which use a object.*function_pointer() or object_pointer->*function_pointer() syntax9.

One example in assignment 2 is CallClipFace, which I used as a workaround for VC6's flaky boost::bind support.

struct CallClipFace : unary_function<Face&, size_t>
{
  const Plane& mPlane;
  CallClipFace( const Plane& plane ) : mPlane( plane ) {}
  size_t operator()( Face& face ) { return mPlane.ClipFace( face ); }
};

The class (yes, a struct is a class) is derived from a std::unary_function template, which defines a number of typedefs the STL relies on. unary_function<Face&, size_t> means that CallClipFace is a unary function, taking a Face& and returning a size_t. The constructor for the struct takes a Plane as a parameter, which will define what plane's ClipFace member function gets called by operator(). That way, when creating a CallClipFace object, we're turning a call to a member function into something that can be used with regular function syntax, and keeps track of bound parameters that you don't have to re-specify at the call point. CallClipFace clipperA (planeA); will clip faces with respect to planeA when clipperA( face ) is invoked.

That way, it can be used in a for_each algorithm, which uses the unary function syntax :

for_each( model.begin(), model.end(), CallClipFace(*this) );

A temporary CallClipFace object gets created, which will clip faces with respect to *this plane (the call is done in a Plane's member function), and this object's operator() gets called for each of the elements contained in the model (between model.begin() and model.end() ).

Now, it can be burdensome to declare a function object class for each and every algorithm call you do . This is where boost::bind comes into play. It generates a function object from a function, member function, or other function object – essentially doing the work described above for you – while letting you modify the way a function takes parameters, 'binding' a number of arguments to fixed values, so that you don't have to provide them again later.

The basic syntax, as described in class is rather simple :

boost::bind( Function, param0, param1, param2 ... ) ;

returns a function object (of an appropriate boost::function type), for which operator() calls,

Function(param0, param1, param2 ...);

If you want to have some parameters still left unspecified, you use the placeholders _1, _2 ... _9 which correspond to the first, second ... ninth argument that will be passed at the call point. Hence

void func(int, int, int);
function1<void, int> f; // unary function, takes an int, returns void10
function2<void, int, int> g; // binary function, takes two ints, returns void

f = bind( func, _1,  5, 10 ); // f(x) calls func( x, 5, 10 );
f = bind( func,  5, _1, 10 ); // f(x) calls func( 5, x, 10 );
f = bind( func, _1, _1, _1 ); // f(x) calls func( x, x,  x );

g = bind( func, _1,  5, _2 ); // g(x, y) calls func( x,  5, y );
g = bind( func, 10, _2, _1 ); // g(x, y) calls func( 10, y, x );
g = bind( func, _2, 10, 10 ); // g(x, y) calls func( y, 10, 10); x is discarded

This is for a normal function, or a static member function. When binding a non-static member function, the first parameter correspond to the base object of the member function.

Plane plane;
function1<size_t, Face&> f; // unary function, takes a Face&, returns a size_t

f = bind( Plane::ClipFace, plane, _1 ); f(x) calls plane.ClipFace(x);
for_each( model.begin(), model.end(), f( *this ) ); // this->ClipFace( face )

Watch out though. The function object returned by boost::bind holds a copy of the bound parameter, not a reference – boost::bind has pass-by-value semantics. Thus the original copy of face will not be affected by calls to g(x), only the copy held by g. To specify that you really want to pass by reference, you have to say it explicitely, using boost::ref or boost::cref.

Face face;
function1<size_t, Plane&> g; // unary function, takes a Plane&, returns a size_t
g = bind( Plane::ClipFace, _1, face );      // g(x) calls x.ClipFace(face); by value.
g = bind( Plane::ClipFace, _1, ref(face) ); // g(x) calls x.ClipFace(face); by ref.

Function objects have several advantages over regular functions



Smart pointers - http://www.boost.org/libs/smart_ptr/shared_ptr.htm

Elements removed from containers are automatically destroyed. However, pointers do not have a destructor. The burden for destroying the object they point to lies, as usual, on the programmer. Smart pointers ease that burden by managing object lifetimes themselves, destroying objects which are not in use anymore.

A smart pointer is a small object that can be used with the same syntax as a pointer, but provides additional functionality11. In Assignment 2, we used boost::shared_ptr<Type>, which is initialised with a pointer to a Type variable, and keeps track of how many copies of itself have been made, calling delete on the pointer it contains when all are gone12.

typedef boost::shared_ptr<Model> spModel;
std::vector<spModel> models;
/* fill the vector */
spModel KeepMe = models[3];
models.clear();
KeepMe->Render();

In the above code, all the models that were kept in the vector will be destroyed, except for the one that is still being used. When the KeepMe variable goes out of scope, or is reset to point to another model, the object it was holding will be deleted since no other spModel points to it. Similarly, if the smart pointer was a member variable (as is the case for mUnclippedModel and mClippedModel) of another object, the pointed-to object would get destroyed along with the (last) object referencing it. There is no need to explicitely delete them in the destructor.



1Granted, this doesn't exactly apply to the C library, which is explicitly part of the C++ library.

2Some iterators may provide more functionality when the container can implement it efficiently.

3We check for != rather than < because it is available for all iterators. List iterators do not implement < as it would involve actually walking through the list nodes until the end is reached, which would be prohibitively slow for such a critical operation.

4Use container::size() to check for the size, and constainer::empty() to check if the container is empty.

5Or a list<Face>::const_iterator if you have a const list<Face>

6It was implicit that pointers act as iterators on C arrays.

7For functions that explicitely expect a function pointer or a function object, you still have to pass an argument of the exact type.

8Consult http://www.function-pointer.org for details on function pointers, pointers to member functions, callback functions...

9As much as people try to do it, it is impossible to cast a pointer to a non-static member function into a pointer to function.

10Here, unlike with std::unary_function, the first parameter is the return type – much like a regular function definition.

11They are not pointers though, functions expecting a pointer will not accept a smart pointer.

12Cycles – A points to B, B (possibly indirectly) points to A will break the reference-count system.