RSS Feed
Download our iPhone app
Browse DevX
Sign up for e-mail newsletters from DevX


The Top 20 C++ Tips of All Time : Page 6

What makes these tips special is that the information they provide usually cannot be found in C++ books or Web sites. For example, pointers to members are one of the most evasive, tricky, and bug-prone issues for even advanced users.

14 to 20: STL and Generic Programming
The Standard Template Library (STL) has revolutionized the way C++ programmers write code. It brings code reuse to new levels of productivity and it saves huge amounts of time that would have otherwise been spent on reinventing wheels. Yet, it's a full-blown framework, with a peculiar jargon and intricate rules that you have to master in order to learn how to use it effectively. To shed some light on certain aspects of STL, I decided to include no less than six tips under this category.

Naturally, the first tip describes the basic constituents of STL and some of its key terms.

The next tip deals with template definitions. As you probably know, templates are the building bricks of STL containers and algorithms. The following three tips describe techniques of using the std::vector container—the most widely-used STL container. We will learn how to properly store pointers to objects in a vector and how to avoid common pitfalls; we will see how to treat a vector object as if it were a built-in array. The fifth tip in this category will show you how to use vectors to imitate a multidimensional array. Finally, the closing tip in this category teaches a very important lesson about the interaction of std::auto_ptr and STL.

Tip 14: Useful STL Terminology
Here are some key terms that you may find useful when reading Standard Template Library (STL) literature and documentation.

A container is an object that stores objects as its elements. Normally, it's implemented as a class template that has member functions for traversing, storing and removing elements. Examples of container classes are std::list and std::vector.

The quality of being generic, or type-independent. The above definition of the term container is too loose because it may apply to strings, arrays and structs, which also store objects. However, a real container isn't limited to a specific data type or a set of types. Rather, it can store any built-in and user-defined type. Such a container is said to be generic. Note that a string can only contain characters. Genericity is perhaps the most important characteristic of STL. The third tip presents the standard base classes for function objects. Since function objects are one of the constituents of generic programming, adhering to standard conventions in their design and implementation will save you a lot of difficulties.

A set of operations applied to a sequence of objects. Examples of algorithms are std::sort(), std::copy(), and std::remove(). STL algorithms are implemented as function templates taking iterators.

An adaptor is a special object that can be plugged to an exiting class or function to change its behavior. For example, by plugging a special adaptor to the std::sort() algorithm, you can control whether the sorting order is descending or ascending. STL defines several kinds of sequence adaptors, which transform a container to a different container with a more restricted interface. A stack, for instance, is often formed from queue<> and an adaptor that provides the necessary push() and pop() operations.

Big Oh Notation
A special notation used in performance measurements of algorithms. The STL specification imposes minimum performance limits on the operations of its algorithms and container operations. An implementation may offer better performance but not worse. The Big Oh notation enables you to evaluate the efficiency of algorithms and containers for a given operation and data structure. An algorithm such as std::find(), which traverses every element is a sequence (in the worst case scenario) has the following notation:

T(n) = O(n). /* linear complexity */

An iterator is an object that functions as a generic pointer. Iterators are used for traversing, adding and removing container elements. STL defines five major categories of iterators:

input iterators and output iterators
forward iterators
bidirectional iterators
random access iterators

Note that this list doesn't represent inheritance relationships; it merely describes the iterator categories and their interfaces. Each lower category is a superset of the category above it. For instance, a bidirectional iterator provides all the functionality of a forward iterators plus additional functionality. Here is a brief summary of the functionality and interfaces for these categories:

Input iterators allow an algorithm to advance the iterator and offer read-only access to an element.

Output iterators allow an algorithm to advance the iterator and offer write-only access to an element.

Forward iterators support both read and write access, but traversal is permitted only in one direction.

Bidirectional iterators allow the user to traverse the sequence in both directions.

Random access iterators support random jumps and "pointer arithmetic" operations, for example:

string::iterator it = s.begin();
char c = *(it+5); /* assign sixth char to c*/

Tip 15: The Location of Template Definitions
Normally, you declare functions and classes in a .h file and place their definition in a separate .cpp file. With templates, this practice isn't really useful because the compiler must see the actual definition (i.e., the body) of a template, not just its declaration, when it instantiates a template. Therefore, it's best to place both the template's declaration and definition in the same .h file. This is why all STL header files contain template definitions.

In the future, when compilers support the "export" keyword, it will be possible to use only the template's declaration and leave the definition in a separate source file.

Tip 16: Standard Base Classes for Function Object
To simplify the process of writing function objects, the Standard Library provides two class templates that serve as base classes of user-defined function objects: std::unary_function and std::binary_function. Both are declared in the header <functional>. As the names suggest, unary_function serves as a base class of function objects taking one argument and binary_function serves as a base class of function objects taking two arguments. The definitions of these base classes are as follows:

template < class Arg, class Res > struct 
 typedef Arg argument_type;
 typedef Res result_type;
template < class Arg, class Arg2, class Res > 
struct binary_function 
 typedef Arg first_argument_type;
 typedef Arg2 second_argument_type;
 typedef Res result_type;

These templates don't provide any useful functionality. They merely ensure that arguments and return values of their derived function objects have uniform names. In the following example, the predicate is_vowel, which takes one argument, inherits from unary_function:

template < class T > 
class is_vowel: public unary_function< T, bool >
 bool operator ()(T t) const
  if ((t=='a')||(t=='e')||(t=='i')||(t=='o')||(t=='u'))
   return true;
  return false;

Tip 17: Storing Dynamically Allocated Objects in STL Containers
Suppose you need to store objects of different types in the same container. Usually, you do this by storing pointers to dynamically allocated objects. However, instead of using named pointers, insert the elements to the container as follows:

class Base {};
class Derived : public Base{};

std::vector <Base *> v;
v.push_back(new Derived);
v.push_back(new Base);

This way you ensure that the stored objects can only be accessed through their container. Remember to delete the allocated objects as follows:

delete v[0];
delete v[1];

Tip 18: Treating a Vector as an Array
Suppose you have a vector of int and function that takes int *. To obtain the address of the internal array of the vector v and pass it to the function, use the expressions &v[0] or &*v.front(). For example:

void func(const int arr[], size_t length );
int main()
 vector <int> vi;
 //.. fill vi
 func(&vi[0], vi.size());

It's safe to use &vi[0] and &*v.front() as the internal array's address as long as you adhere to the following rules: First, func() shouldn't access out-of-range array elements. Second, the elements inside the vector must be contiguous. Although the C++ Standard doesn't guarantee that yet, I'm not aware of any implementation that doesn't use contiguous memory for vectors. Furthermore, this loophole in the C++ Standard will be fixed soon.

Tip 19: Dynamic Multidimensional Arrays and Vectors
You can allocate multidimensional arrays manually, as in:

int (*ppi) [5]=new int[4][5]; /*parentheses required*/
/*fill array..*/
ppi[0][0] = 65;
ppi[0][1] = 66;
ppi[0][2] = 67;
delete [] ppi;

However, this style is tedious and error prone. You must parenthesize ppi to ensure that the compiler parses the declaration correctly, and you must delete the allocated memory. Worse yet, you can easily bump into buffer overflows. Using a vector of vectors to simulate a multidimensional array is a significantly superior alternative:

#include <vector>
#include <iostream>
using namespace std;
int main()
 vector <vector <int> > v; /*two dimensions*/
 v.push_back(vector <int>()); /*create v[0]*/
 v.push_back(vector <int>()); /*create v[1]*/
 v[0].push_back(15); /*assign v[0][0]*/
 v[1].push_back(16); /*assign v[1][0]*/

Because vector overloads operator [], you can use the [][] notation as if you were using a built-in two-dimensional array:

cout << v[0][0]; 
cout << v[1][0];  

The main advantages of using a vector of vectors are two: vector automatically allocates memory as needed. Secondly, it takes care of deallocating memory so you don't have to worry about potential memory leaks.

Tip 20: Why You Shouldn't Store auto_ptr Objects in STL Containers
The C++ Standard says that an STL element must be "copy-constructible" and "assignable." These fancy terms basically mean that for a given class, assigning and copying one object to another are well-behaved operations. In particular, the state of the original object isn't changed when you copy it to the target object.

This is not the case with auto_ptr, though: copying or assigning one auto_ptr to another makes changes to the original in addition to the expected changes in the copy. To be more specific, the original object transfers ownership of the pointer to the target, thus making the pointer in the original null. Imagine what would happen if you did something like this:

std::vector <auto_ptr <Foo> > vf;/*a vector of auto_ptr's*/
// ..fill vf
int g()
  std::auto_ptr <Foo> temp=vf[0]; /*vf[0] becomes null*/

When temp is initialized, the pointer of vf[0] becomes null. Any attempt to use that element will cause a runtime crash. This situation is likely to occur whenever you copy an element from the container. Remember that even if your code doesn't perform any explicit copy or assignment operations, many algorithms (std::swap(), std::random_shuffle() etc.) create a temporary copy of one or more container elements. Furthermore, certain member functions of the container create a temporary copy of one or more elements, thereby nullifying them. Any subsequent attempt to the container elements is therefore undefined.

Visual C++ users often say that they have never encountered any problems with using auto_ptr in STL containers. This is because the auto_ptr implementation of Visual C++ (all versions thereof) is outdated and relies on an obsolete specification. When the vendor decides to catch up with the current ANSI/ISO C++ Standard and change its Standard Library accordingly, code that uses auto_ptr in STL containers will manifest serious malfunctions.

To conclude, you shouldn't use auto_ptr in STL containers. Use either bare pointers or other smart pointer classes instead of auto_ptr (such classes are available at www.Boost.org).

Danny Kalev is the DevX C++ Pro. Reach him via email at dannykk@inter.net.il.
Email AuthorEmail Author
Close Icon
Thanks for your registration, follow us on our social networks to keep up-to-date