C++ | |
BEGINNING C++ | |
THE BOOST WEB SITE
GUARANTEES ABOUT THE SIZES OF INTEGRAL TYPESTIME REPRESENTATIONA VIRTUAL MEMBER FUNCTION CAN BECOME PURE VIRTUAL IN A DERIVED CLASS
GET A FREE C++ COMPILER FOR WINDOWSINTEGRAL TYPES WITH PORTABLE SIZES
MINIMIZE THE USE OF DYNAMIC MEMORY ALLOCATION
THE MEMORY LAYOUT OF IDENTICAL STRING LITERALS
UNDERSTANDING MEMORY ALIGNMENT
CONSTRUCTING OBJECTS ON PREALLOCATED CHAR ARRAYS
DELETING MULTIDIMENSIONAL ARRAYS
USE STATIC ALLOCATION FOR BUFFERS WITH A FIXED SIZE
OVERLOADING THE FUNCTION CALL OPERATOR
THE ROLE OF IMPLICITLY DECLARED CONSTRUCTORS
THE C_STR() AND DATA() MEMBER FUNCTIONS OF STD::STRING
ALTERNATIVE REPRESENTATIONS OF OPERATORS C++ defines textual representations for logical operators. Platforms and hardware equipment that do not support all the special characters that are used as logical operators can use these alternative representations. The alternative representations are also useful when you want to transfer source files in a portable form to other countries and locales, in which special characters such as ^ and ~ are not part of the national character set. The following list contains the alternative keywords and their equivalent operators:
COPY CTOR AND ASSIGNMENT OPERATOR Although the copy constructor and assignment operator perform similar operations, they are used in different contexts. The copy constructor is invoked when you initialize an object with another object: string first "hi"; string second(first); //copy ctor On the other hand, the assignment operator is invoked when an already constructed object is assigned a new value: string second; second = first; //assignment op Don't let the syntax mislead you. In the following example, the copy ctor--rather than the assignment operator--is invoked because d2 is being initialized: Date Y2Ktest ("01/01/2000"); Date d1 = Y2Ktest; /* although = is used, the copy ctor invoked */ EVALUATION ORDER OF A MEMBER-INITIALIZATION LIST
Whenever you use a member-initialization list, the compiler transforms
the list so that its members are laid down in the order of declaration
of the class data members. For example, the following class declares
two data members: a and b. However, the constructor's
member-initialization list first initializes the member b and then a:
GUIDELINES FOR WRITING PORTABLE CODE Contrary to what most people believe, portability doesn't guarantee that the same code can be compiled and run on every platform without any modifications. Although it's sometimes possible to write 100 percent portable code, in nontrivial applications such code is usually too complex and inefficient. A better approach is to separate platform-specific modules from the platform-independent parts of the entire application. GUI components, for instance, tend to be very platform specific, and therefore should be encapsulated in dedicated classes. On the other hand, business logic and numeric computations are more platform independent. Therefore, when the application is to be ported, only the GUI modules need to be rewritten, while the rest of the modules can be reused. USING A TEMPLATE AS A TEMPLATE'S ARGUMENT
You can use a template as a template's argument. In the following
example, a mail server class can store incoming messages in a vector
of messages, where each message is represented as a vector of bytes:
BEFORE PROFILING YOUR APPS... If you intend to optimize your software's performance, be sure to profile the release version rather than the debug version. The debug version of the executable contains additional code (about 40 percent extra compared to the equivalent release executable) for symbol lookup and other debug "scaffolding." In addition, most compilers have distinct implementations of the run-time library--one for the debug version and one for the release version. For example, the debug version of operator new initializes the memory it allocates with a unique value so that memory overruns can be detected automatically. Conversely, the release version of new doesn't initialize the allocated block. Furthermore, an optimizing compiler may have already optimized the release version of an executable in several ways, such as eliminating unneeded variables, loop unrolling, storing variables in the CPU registers, and function inlining. These optimizations are not applied to the debug version. Therefore, you can't deduce from a debug version where the actual bottlenecks are located. To conclude, debugging and optimization are two distinct operations. Use the debug version to chase bugs and logical errors; profile the release version to optimize it. UNDERSTANDING REFERENCE COUNTING A reference counting class counts how many object instances have an identical state. When two or more instances share the same state, the implementation creates only a single copy and counts the number of existing references to this copy. For example, an array of strings can be represented as a single string object that holds the number of elements in the array. Since initially the array elements share the same state (they all are empty strings), only a single object is needed to represent the array, regardless of its size. When one of the array elements changes its state (the program writes a different value to it), the reference counting object creates one more object--this is called "copy on write." Under some conditions, reference counting can boost performance considerably both in terms of run-time speed and memory usage. For this reason, many implementations of std::string use copy on write. TEMPLATES AND INHERITANCE
A common mistake is to assume that a vector < Derived > is like a
vector < Base > when Derived is a subclass of Base. For example:
TEMPLATES AND INHERITANCE
A common mistake is to assume that a vector < Derived > is like a
vector < Base > when Derived is a subclass of Base. For example:
WHEN TO USE A VOID-CAST
Some rigorous compilers issue a warning message if you ignore a
function's return value. For example:
BEGIN() AND END() MEMBER FUNCTIONS OF STL CONTAINERS
All STL containers provide the begin() and end() pair of member
functions. begin() returns an iterator that points to the first
element of the container. For example:
CONTAINER'S REALLOCATION AND ITS CONSEQUENCES
When a container has exhausted its free storage and additional
elements have to be inserted to it, the container reallocates itself.
The reallocation process consists of four steps. First, a new memory
buffer large enough to store the container is allocated. Second, the
container copy-constructs existing elements on the new memory
location. Third, the destructors of the original elements are
successively invoked. Finally, the original memory buffer is released.
Clearly, frequent reallocations can impose a significant performance
overhead. However, some techniques can minimize--and sometimes
avoid--reallocation. For example, when you create a vector object, you
can specify its size in advance:
CONTAINER'S REALLOCATION AND ITERATOR INVALIDATION
When a container reallocates its elements, their addresses change
correspondingly. Consequently, the values of existing pointers and
iterators are invalidated:
DISPLAYING THE LITERAL REPRESENTATION OF BOOL VARIABLES
By default, iostream objects display bool variables as 0 and 1. You
can override the default setting by inserting the formatting flag
boolalpha to the object stream. Subsequently, 'false' and 'true' will
be displayed instead of 1 and 0:
THE COPY ALGORITHM
The Standard Library provides a generic function, copy(), which you
can use for copying a sequence of objects to a specified target. The
first and the second arguments of copy() are const iterators that mark
the beginning and the end of the sequence being copied (or a fragment
of that sequence). The third argument points to a container into which
the sequence is to be copied. The following example shows how to copy
the elements of a list into a vector using the copy() algorithm:
AVOID MISUSES OF EXCEPTION HANDLING
Although you can use exceptions instead of for-loops and break
statements, doing so is not a recommended programming practice.
Consider the following demo application that prompts the user to enter
data until a certain condition is met:
DEFINING A STATIC MEMBER FUNCTION A static member function in a class can access only other static members of its class. Unlike ordinary member functions, a static member function can be invoked even when no object instance exists: class Clock { public: static void showTime(); }; int main() { stat:: showTime(); /* called without an object */ stat s; s. showTime(); /* called from an object */ } Static members are used when all other data members of an object are also static, when the static function does not depend on any other object member, or, simply, when a global function is undesirable (in the pre-namespaces era, this was the predominant method of preventing global functions). FORWARD-DECLARING I/O CLASSES AND TEMPLATES The standard header < iosfwd > contains forward declarations of the standard I/O classes and templates. This header is sufficient to refer to any of the I/O classes and templates, but not to apply operation to them. For example: #include < iosfwd > using namespace std; class C { public: friend ostream & operator < < (ostream & os, const C & d); }; ostream & operator < < (ostream & os, const C & d); In the example, the declaration of the friend function does not need a complete definition of the ostream class; a forward declaration is sufficient in this case. Therefore, < iosfwd > is #included instead of the full-blown < iostream >. The result is a significantly reduced compilation time. INTERACTING WITH THE OPERATING SYSTEM In general, API functions and classes enable you to interact with the operating system. Sometimes, however, it is much simpler to execute a system command directly. For this purpose, you can use the standard function system() that takes a const char * argument containing a shell command. For example, on a DOS/Windows operating system, you can display the files in the current directory like this: #include < cstdlib > int main() { system("dir"); /* execute the "dir" command */ } OVERLOADING OPERATORS FOR ENUM TYPES For some enum types, it may be useful to overload operators such as ++ and -- that can iterate through the enumerator values. You can do it like this: enum Days {Mon, Tue, Wed, Thur, Fri, Sat, Sun}; Days & operator++(Days & d, int) /* int indicates postfix ++ */ { if (d == Sun) return d = Mon; /* rollover */ int temp = d; return d = static_cast < Days > (++temp); } int main() { Days day = Mon; for (;;) /* display days as int's */ { cout < < day < < endl; day++; if (day == Mon) break; } } STRING LITERALS AND TEMPLATE ARGUMENTS A template can take the address of an object with external linkage as an argument. Consequently, you cannot use a string literal as a template argument since string literals have internal linkage. For the same reason, you cannot use local pointers for that purpose, either. For example: template < class T, const char * > class A {/*...*/}; extern const char *global_ptr; void array_user() { const char * p ="illegal"; A < int, "invalid" > aa; /* error, string literal used as argument */ A < int, p > ab; /* error, p has internal linkage */ A < int, global_ptr > ac; /* OK, global_ptr has external linkage */ } THE "BIG THREE RULE" VERSUS THE "BIG TWO RULE" The famous "Big Three Rule" says that if a class needs any of the Big Three member functions (copy constructor, assignment operator, and destructor), it needs all of them. In general, this rule refers to classes that allocate memory from the free store. However, many other classes require only that the Big Two (copy constructor and assignment operator) be defined by the user; the destructor, nonetheless, is not always required. Examine the following example: class Year { private: int y; bool cached; /* has this instance been cached? */ public: Year(int y); Year(const Year & other) /* make sure 'cached' isn't copied */ { y = other.getYear(); } Year & operator =(const Year & other) /* make sure 'cached' isn't copied */ { y = other.getYear(); return *this; } int getYear() const { return y; } }; /* no destructor is required for class Year */ Class Year does not allocate memory from the free store, nor does it acquire any other resources during its construction. A destructor is therefore unnecessary. However, the class needs a user-defined copy constructor and assignment operator to ensure that value of the member 'cached' is not copied, because it is calculated for every individual object separately. TWO FORMS OF DYNAMIC_CAST The operator dynamic_cast comes in two flavors. One uses pointers and the other uses references. Accordingly, dynamic_cast returns a pointer or a reference of the desired type when it succeeds. However, when dynamic_cast cannot perform the cast, it returns a null pointer, or in case of a reference, it throws a std::bad_cast exception: void f(Shape & shape) { Circle * p = dynamic_cast < Circle * > ( & shape); /* is shape a Circle? */ if ( p ) { p->fillArea (); } else {} /* shape is not Circle */ } In this example, dynamic_cast examines the dynamic type of Shape by casting its address to a pointer to Circle. If the cast is successful, the resultant pointer is used as a pointer to Circle. In contrast, the next example uses a reference dynamic_cast. You should always place a reference dynamic_cast within a try-block and include a suitable catch-statement to handle the potential std::bad_cast exception, as follows: #include < typeinfo > /* for std::bad_cast */ void f(Shape & shape) try { Circle & ref = dynamic_cast < Circle & > (shape); ref.fillArea(); /* successful cast */ } catch (std::bad_cast & bc) /* shape is not a Circle */ {/*..*/} } WHAT ARE LVALUES AND RVALUES? An object is a contiguous region of memory storage. An lvalue (pronounced L value) is an expression that refers to such an object (the original definition of lvalue referred to "an object that can appear on the left-hand side of an assignment"). An expression that can appear on the right-hand side of an expression (but not on its left-hand side) is an rvalue. For example: #include < string > using namespace std; int & f(); void func() { int n; char buf[3]; n = 5; /* n is an lvalue; 5 is an rvalue */ buf[0] = 'a'; /* buf[0] is an lvalue, 'a' is an rvalue */ string s1 = "a", s2 = "b", s3 = "c"; /* "a", "b", "c" are rvalues */ s1 = /* lvalue */ s2 +s3; /* s2 and s3 are lvalues that are implicitly converted to rvalues */ s1 = string("z"); /* temporaries are rvalues */ int * p = new int; /* p is an lvalue; 'new int' is an rvalue */ f() = 0; /* a function call that returns a reference is an lvalue */ s1.size(); /* otherwise, a function call is an rvalue expression */ } An lvalue can appear in a context that requires an rvalue; in this case, the lvalue is implicitly converted to an rvalue. However, an rvalue cannot be converted to an lvalue. Therefore, it is possible to use every lvalue expression in the example as an rvalue, but not vice versa. BOOSTING PERFORMANCE OF LEGACY SOFTWARE When you port pure C code to a C++ compiler, you may discover slight performance degradation. This is not a fault in the programming language or the compiler, but a matter of compiler setting. To regain the same (or better) performance as with a C compiler, simply disable the compiler's RTTI and exception-handling support. Why is this? To support these features, a C++ compiler appends additional "scaffolding" code to the program. Consequently, the program's speed may be reduced and its size may increase. The additional overhead incurred by RTTI and exception handling ranges from five percent to 20 percent, depending on the compiler. When you're using pure C, this overhead is unnecessary, so you can safely disable RTTI and exception-handling support. However, you should not attempt to apply this tweak to C++ code or C code that uses any C++ constructs, such as operator new. CALLING A MEMBER FUNCTION FROM A CONSTRUCTOR You can safely call member functions--virtual and nonvirtual alike--of an object from its constructor. It is guaranteed that the invoked virtual is the one defined for the object whose constructor is executing. Note, however, that virtual member functions of an object derived from the one whose constructor is executing are not invoked: class A { public: virtual void f(); virtual void g(); }; class B: public A { public: void f (); /* overrides A::f() */ B() { f(); /* B::f() */ g(); /* A::g()*/ } }; class C { public: void f (); /* overrides B:::f() */ }; B * p = new B; B's constructor calls f() and g(). The calls are resolved to B::f() and A::f(), respectively, because B overrides f() but it doesn't override g(). Note that C::f() is not called from B's constructor. DYNAMIC_CAST AND ACCESS SPECIFICATIONS Operator dynamic_cast fails when it cannot convert its argument to the desired type. Usually, such a failure results from an attempt to cast an object to a nonrelated class. However, the int may also be the result of an attempt to cast a derived object to a private base class. For example: class Container {/*..*/ }; class Allocator{/*..*/}; class Stack: public Container, private Allocator {/*..*/ }; int main() { Stack s; /* attempt to cast a pointer to Stack to a pointer to Allocator */ Allocator* p = dynamic_cast < Allocator* > ( & s); /* runtime failure */ } In the example above, dynamic_cast fails because Stack is not publicly derived from Allocator. EFFICIENCY OF STRING COMPARISONS Class std::string defines three versions of the overloaded == operator: bool operator == (const string & l, const string & r); bool operator == (const char* l, const string & r); bool operator == (const string & l, const char* r); This proliferation may seem redundant, since std::string has a constructor that automatically converts a const char * to a string object. Thus, one could make do with only the first version of operator ==, which in turn converts a char * to a temporary string object, and then performs the comparison. However, the overhead of creating a temporary string can be unacceptable under some circumstances. The C++ Standardization Committee's intent was to make comparison operation of std::string as efficient as possible. Therefore, the additional overloaded versions were added to allow direct comparisons, without the additional overhead of temporaries. FORWARD DECLARATIONS AND TYPEDEF NAMES
It is illegal to use forward declarations with typedef names:
class string; /* illegal, string is a typedef name */
void f(string & s);
Even a typename won't do here:
typename std::string; /* still illegal */
void f(std::string& s);
Can you see what the problem is with these forward declarations?
std::string is not a class, but a typedef name defined in the standard
header < string > like this:
/* note: this is a simplified form */
typedef basic_string NAMESPACES DO NOT IMPOSE ANY OVERHEAD The use of namespaces in your application does not incur runtime or memory overhead. Therefore, you can use namespaces in embedded systems and time-critical applications without hesitation. PREFER OBJECT INITIALIZATION TO ASSIGNMENT In general, initializing an object is more efficient than assignment. Consider the following code snippet: string s1 = "Hello "; string s2 = "World"; string both; both = s1+s2; /* assignment rather than initialization; inefficient */ The compiler transforms the statement both = s1+s2; into this: string temp (s1 + s2); /* store result in a temporary */ both = temp; /* assign */ temp.::~string(); /* destroy temp */ Using initialization rather than assignment is more efficient: string both = s1+s2; /* initialization */ This time, the compiler transforms the expression both = s1+s2; into this: string both (s1 + s2); In other words, the construction and destruction of a temporary are avoided in this case. To conclude, object assignment requires the creation and destruction of a temporary, whereas initialization does not. Whenever you have the choice, choose object initialization rather than assignment. THE ROLE OF ALLOCATORS Every STL container uses an allocator. Allocators encapsulate the low-level details of the memory model of a given platform: They hide the size of pointers, reallocation policy, block and page size, heap location, etc. Containers are therefore rather portable, because a different allocator type can be plugged to them, depending on the platform. Your STL implementation provides an allocator that is suitable for your platform and application's needs. However, some applications may require custom-made allocators. VIRTUAL FUNCTIONS SHOULD NOT BE DECLARED PRIVATE It is customary to extend virtual functions in a derived class by first invoking the base's version of that function and then extending it with additional functionality. Therefore, there is little point in declaring a virtual member function private--by doing so, you disable the user's ability to extend it in a derive class. REPORTING AN ERROR DURING OBJECT CONSTRUCTION Constructors have no return value (not even void). Therefore, you have to use alternative techniques of reporting errors during object construction. There are three common techniques of reporting such errors. The first technique uses C-style error handling: When an error occurs during construction, the constructor assigns an error value to a global variable, which is later examined. Although this is the least preferable technique, it can be useful when combining legacy code written in C with new C++ code: int errcode = OK; /* global flag */ Date::Date(const char *datestr) { if (isValid(datestr) == false) errcode = EINVALIDDATE; ... } int main() { Date d("02/29/1999"); /* invalid */ if (errcode == OK) /* proceed normally */ { ... } else {..} /* handle the error */ } Date's constructor turns on the global flag errcode to indicate a runtime error. Main() subsequently checks the flag's value and handles the exception. THROWING EXCEPTIONS TO REPORT AN ERROR DURING OBJECT CONSTRUCTION Many implementations still don't support exception handling very well. Therefore, the methods of using global variables and passing a parameter to the constructor can be used instead. However, on implementations that support exception handling, throwing an exception is the preferred method of reporting runtime errors during construction. The advantages of exceptions are better support for object-oriented programming, simpler and more readable code, safety, and automation. The constructor in the following example throws an exception to report a runtime error: class X { public: X(const char* description); ... }; Date::Date(const char *datestr) throw (X) { if (isValid(datestr) == flase) throw X("invalid date string"); ... } int main() { try { Date d("02/29/1999"); // ... proceed normally } catch (X & exception) { /* ... handle the exception */ } } USING PARAMETERS TO REPORT AN ERROR DURING OBJECT'S CONSTRUCTION Using global variables to indicate runtime errors has many disadvantages. An alternative method of reporting runtime errors during object construction is to pass an additional argument to the constructor. The constructor in turn assigns an appropriate value to that argument that indicates success or failure. For example: Date::Date(const char *datestr, int & status) { if (isValid(datestr) == flase) status = EINVALIDDATE; ... } int main() { int status; Date d("02/29/1999", status); if (status == OK) /* proceed normally */ { ... } } FUNCTION ARGUMENTS EVALUATION ORDER The order of evaluation of function arguments is unspecified. This means that for a function that takes the arguments (a, b, c), any permutation of this set is a valid argument evaluation sequence. To demonstrate that, suppose you write a function f() and you call it like this: bool x; /* global */ f( g(false), 1, x, 3); Suppose also that g() changes the value of x. One cannot tell which value of x is passed to f() as its third argument--it could be either the value of x before or after the call to g(false). Therefore, writing code that relies on a specific evaluation order of function arguments is bad programming practice and should be avoided. THE NEW LONG LONG DATA TYPE The long long type denotes an integral data type that is at least as large as a long int. On 32-bit architectures and 64-bit architectures, long long occupies 64 bits (eight bytes). Although long long is not defined by the ANSI/ISO C++ standard yet, it is defined by the C9X draft standard and, in practice, many platforms and compilers already support it. As with other integral types, a long long has an unsigned counterpart: unsigned long long distance_to_star; The suffix ll or LL can be added to a literal integer to indicate a long long type: const long long year_light_in_km = 9460800000000LL; Likewise, the suffixes ull and ULL can be added to a literal integer to indicate an unsigned long long type. ALTERNATIVE STL IMPLEMENTATION The STLport organization offers an alternative implementation of the Standard Template Library that you can install instead of the original STL implementation supplied with your compiler. The creators of this implementation claim that it's fully compatible with Microsoft's Visual C++ 5.0 and 6.0, as well as many other platforms. You can download the STLport implementation from http://www.stlport.org/index.shtml The site contains installation directions and extensive documentation about the STLport version of STL. RETURNING A VOID EXPRESSION FROM A VOID FUNCTION Examine the following code snippet. Does your compiler accept it? void a_func_returning_void(); void func() { return a_func_returning_void(); /* returning a value from a void function? */ } At present, most compilers will not accept this code, although it's perfectly legal. The problem is that until not long ago, returning an expression from a function that returns void was an error, even if the returned expression itself was void. This restriction was changed recently: A function with a void return type may now return an expression that evaluates to void. This change was required to enable better support of generic algorithms in the STL. STATIC CLASS MEMBERS MAY NOT BE INITIALIZED IN A CONSTRUCTOR You cannot initialize static data members inside the constructor or a member-initialization list: class File { private: static bool locked; public: File::File(): locked(false) {} /*error*/ }; Although compilers flag such ill-formed initializations as errors, programmers often wonder why this is an error. Remember that a constructor is called as many times as the number of objects created, whereas a static data member may be initialized only once, since it is shared by all the class objects. Therefore, static member initialization should be done outside the class, as in this example: class File {/*..*/}; File::locked = false; /* OK */ Alternatively, for const static members of an integral type, the Standard now allows in-class initialization as follows (note that some compilers don't support this feature yet): class Screen { private: const static int pixels = 768*1024; /* OK */ public: Screen() {/*..*/} ... }; CATCH EXCEPTIONS BY REFERENCE Although you can catch an exception by value, as in catch( Exception except) /* by value */ { /* ... */ } it's better to catch an exception by reference: catch( Exception & except) /* by reference */ { /* ... */ } Catching an exception by reference has several advantages. First, pass by reference is more efficient, since it avoids unnecessary copying of objects. Second, you avoid slicing of derived exception objects. Finally, you ensure that modifications applied to the exception within a catch-clause are preserved in the exception object when it is rethrown. DATA AGGREGATES The term "aggregate" refers to an array or a class with no constructors, no private or protected data members, no base classes, and no virtual functions. In other words, an aggregate is a POD (Plain Old Data) object or an array thereof. Aggregates differ from class objects and arrays of class objects in several ways. In particular, you can initialize every aggregate of every type and size by the '={0};' initializer list. This initializer list guarantees that the entire aggregate is zero-initialized: struct Employee { int ID; int rank char name[12]; int salary; }; /* using the '={0}' initializer to initialize aggregates */ Employee emp = {0}; /* all emp members are zero-initialized */ Employee emp_arr[10] = {0}; /* all array elements are zero-initialized */ Note that nonaggregate objects and arrays cannot be initialized this way; they are initialized by a constructor. GET A FREE COPY OF THE GNU C/C++ COMPILER 2.95 The GNU project focuses on the development and distribution of open source software. The newly released GCC 2.95 is the latest version of GNU C/C++ compiler. Release 2.95 includes numerous bug fixes, new optimizations, and support for new platforms. In addition, its standard compliance has been improved. You can download GCC 2.95 from the GNU site at http://www.gnu.org/software/gcc/gcc-2.95/gcc-2.95.2.html THE C++ EXCEPTION HANDLING MODEL In a resumptive model, after an exception has been handled, the program continues execution at the point where the exception was thrown. The C++ exception-handling model is nonresumptive, though--program execution resumes at the next statement following the catch-block. This is a source of confusion among C++ novices, who mistakenly assume that the program automatically returns to the point where an exception was thrown from, but it doesn't. THE CC++ PARALLEL PROGRAMMING LANGUAGE CC++ (this is not a typo) is a parallel programming language based on C++. A free CC++ compiler as well as an overview of CC++ features, syntax, and supported platforms can be found at http://globus.isi.edu/ccpp/ Although CC++ is not standardized yet, it's interesting to examine the elegance of its straightforward and relatively simple handling of parallel processing, resource locking and unlocking mechanisms, and its platform-neutrality. WHY THE COPY CONSTRUCTOR'S PARAMETER MUST BE PASSED BY REFERENCE The canonical form of a copy constructor of a class called C is C::C(const C & rhs); You can also declare the copy constructor's parameter as follows: C::C(C & rhs); /*non-const, OK*/ That is, the const specifier is not mandatory. Note, however, that the parameter may not be passed by value: C::C(const C rhs); /* Error */ Can you see why the parameter must be passed by reference? Whenever you pass an object by value, you implicitly invoke its copy constructor (because the object being passed by value is in fact a copy of the original argument). Attempting to pass a copy constructor's argument by value will result in an infinite recursion: The copy constructor calls another copy constructor, which in turn calls another copy constructor, and so on. To avoid this, C++ requires that a copy constructor's parameter be passed by reference. INITIALIZING ARRAYS IN A CLASS You cannot initialize an array class member by a member-initialization list. For example: class A { private: char buff[100]; public: A::A() : buff("") /* error */ {} }; The following forms won't compile either: A::A() : buff('\0') {} /* ill-formed */ A::A() : buff(NULL) {} /* ill-formed */ Instead, you should initialize arrays inside the constructor body: A::A() { memset(buff, '\0', sizeof(buff)); } OVERLOADED SUBSCRIPT OPERATOR SHOULD RETURN A REFERENCE When you overload the operator [], remember that its non-const version should return an object by reference, not by value. Otherwise, you won't be able to use it in assignment expressions. For example: class IntArray { private: /*...*/ public: int & operator [] (int index); /*return a reference */ }; IntaArray iar; iar[0] = 1; /*to make this work, operator [] must return int & */ OPTIMIZING LOOPS Consider the following code: class Foo{/*..*/}; std::vector < Foo > v; for (int j =0; j < MAX; j++) { v.push_back ( Foo() ); /* using a temporary */ } This for loop is very inefficient: It constructs and destroys a temporary Foo object on every iteration. You can easily avoid the unnecessary overhead of constructing and destroying this object if you declare it outside the loop: Foo something; // avoiding a temporary for (int j =0; j < MAX; j++) { v.push_back ( something ); } This improved version constructs and destroys the object only once, regardless of how many times the loop executes. In general, you should avoid declaring temporary objects inside loops. Instead, create them outside the loop. THE USEFULNESS OF PTRDIFF_T C and C++ define a special typedef for pointer arithmetic, ptrdiff_t, which is a platform-specific signed integral type. You can use a variable of type ptrdiff_t to store the result of subtracting and adding pointers. For example: #include < stdlib.h > int main() { int buff[4]; ptrdiff_t diff = ( & buff[3] ) - buff; /*diff = 3*/ diff = buff -( & buff[3] ); /*diff = -3*/ } There are two advantages in using ptrdiff_t instead of int. First, the name ptrdiff_t is self-documenting and helps the reader understand that the variable is used in pointer arithmetic. Second, ptrdiff_t is portable: Its underlying type may vary across platforms, but you don't need to make changes in the source code when porting your software to other platforms. USE EXPLICIT ACCESS SPECIFIERS IN A CLASS DECLARATION In many contexts, C++ provides default access specifiers when you don't spell them out explicitly: struct Employee{ /*..*/}; class Manager : Employee /* implicit public inheritance */ { int rank; /* implicitly private */ bool StockHolder; /* ditto */ }; Relying on the default access specifiers is not recommended, though. Always spell out the access specifier explicitly: class Manager : public Employee { private: int rank; bool StockHolder; }; class BoardMemebr : private Manager {/*..*/}; By using explicit access specifiers, you ensure that the code is more readable and you avoid future maintenance problems. Imagine that the creator of struct Employee decides one day to change it into a class. Consequently, Employee will become a private base class of Manager. A COMMON MISTAKE IN TEMPLATE INSTANTIATION The type that a template takes as an argument must have external linkage. In other words, you cannot instantiate a template with a locally declared type: int main() { struct S { int current_state; char description[10]; }; queue < S > qs; /* 1. compilation error */ } The line numbered 1 causes a compilation error because it attempts to instantiate a template with a locally declared type, S. S has no linkage, and as such, it cannot serve as a valid template argument. To fix this, you should move the declaration of struct S outside of main(): struct S { /* ... */ }; int main() { queue < S > qs; /* now OK */ } AVOID TYPEDEF'S IN STRUCTS DEFINITIONS In olden days, before C++ appeared, it was customary to declare a struct as a typedef: typedef struct DATE_TAG { int day; int month; int year; } Date; /* 'Date' is a typedef */ This way, one could create an instance of the struct without having to use the keyword 'struct': /* C code */ Date date; /* 'struct' not required */ struct DATE_TAG another_date; In C++, the use of a typedef in this context is unnecessary because you don't need the elaborated type specifier (for example, struct, union, and class) to create an instance: // C++ code DATE_TAG another_date; Therefore, avoid declaring structs as typedef names in C++ code. FUNCTION SIGNATURES A function's signature provides the information needed to perform the overload resolution. The signature consists of the function's parameter list and their ordering. Note that a function's return type is not considered part of its signature, and it does not participate in overload resolution. GETTING ALONG WITHOUT NULL REFERENCES In C, algorithms such as bsearch() and lfind() return a null pointer to indicate that the sought-after element wasn't found. Unlike a pointer, a reference must always be bound to a valid object. Therefore, a reference can never be null. I've often seen the following dreadful hack as an attempt to fabricate a null reference: int & locate(int * pi, int size, int value) { if (find_int(pi, size, value) != NULL) // ... else return *((int *) (0)); /* very bad */ } The return statement fakes a null reference by binding a reference to a dereferenced null pointer. This hack violates two C++ rules: The function locate() returns a reference to a local automatic variable. The results of using such a reference are undefined (this is called a "dangling reference"). Worse yet, even if locate() used a non-automatic variable to avoid the dangling reference problem, any attempt to use the returned reference would still cause a runtime crash because you cannot dereference a null pointer. Unfortunately, many compilers will accept this code, letting the user detect its disastrous results at runtime. You can resolve the lack of null references with several approaches. Throwing an exception is one of them. However, the overhead and complexity of exception handling may be overkill for simple functions such as locate(). In this case, you can return a reference to a special object (such as a negative integer when an array subscript cannot be found): int & locate(int * pi, int size, int value) { static int unfound = -1; if (find_int(pi, size, value) != NULL) // ... else return unfound; /* fine */ } INITIALIZE CLASS'S DATA MEMBERS EASILY Suppose you want to initialize all the data members of class Book inside its constructor: class Book { private: char ISBN[11]; int year char name[100]; public: virtual ~Book(); Book(); }; One way to do that is by assigning a value to every member inside the constructor body. However, this is tedious. Don't be tempted to use memset() to initialize the members: Book::Book() { memset(this, 0, sizeof (*this)); /* very bad idea */ } memset() also initializes the object's virtual table pointer. Consequently, the program will crash at runtime. Instead, remove all the class's data members to a dumb struct that doesn't have any virtual member functions and add a constructor to the struct: struct BookData { char ISBN[11]; /*... rest of the data members */ BookData() {memset(this, 0, sizeof (*this)); } /* now OK, struct has no vptr */ }; Next, derive Book from that struct using private inheritance: class Book : private BookData {...}; And that's all. Now every data member of Book is automatically initialized by BookData's constructor. OVERLOADING OPERATOR + PROPERLY The built-in operator + takes two operands of the same type and returns their sum without changing their values. In addition, + is a commutative operator: You can swap the operands' positions and get the same result. Therefore, an overloaded version of operator + should reflect all these characteristics. You can either declare operator + as a member function of its class or as a friend function: class Date { public: Date operator +(const Date & other); /* member */ }; class Year { friend Year operator+ (const Year y1, const Year y2); /* friend */ }; The friend version is preferred because it reflects symmetry between the two operands. Because built-in + does not modify any of its operands, the parameters of the overloaded + are const. Finally, overloaded + should return the result of its operation by value, not by reference. THE DANGERS OF OBJECT SLICING Passing by value a derived object to a function may cause a problem known as "object slicing"--every additional data member declared in the derived object is sliced from the copy that the function receives. Note also that the dynamic binding mechanism is inoperative in sliced objects: class Date { private: int year, month, day; public: virtual void Display() const; /* mm-dd-yy */ }; class DateTime: public Date { private: int hrs, min, sec; /* additional members; might be sliced off */ public: void Display() const; /* mm-dd-yy hh:mm:ss */ }; void f(Date b) /* by value */ { b.Display(); /* no dynamic binding; calls Date::Display() */ } int main() { DateTime dt; f(dt); /* sliced */ } Object slicing may result in undefined behavior. Therefore, pass them by reference whenever possible. USES OF THE OFFSETOF MACRO The standard macro offsetof (defined in < stdlib.h > ) calculates the offset of a struct member. The result is the number of bytes between the beginning of the struct and that particular member. The following example uses this macro to calculate the offset of two data members in the struct S: #include < stdlib.h > struct S { int a; int b; void * p; }; int main() { int n = offsetof(S, a); /*n = 0 */ n = offsetof(S, p); /* n = 8 */ } Note that offsetof works with POD (Plain Old Data) structs and unions. The result of applying this macro to a field that is a static data member of a class is undefined. Likewise, applying offsetof to a class that has virtual functions, base classes, or template members is undefined, as well. VISIT SILICON GRAPHIC'S STL PAGES Silicon Graphics hosts a Web site dedicated to Standard Template Library (STL). This site documents all of the classes, functions, and concepts in the SGI implementation of STL. Each page describes a single component and includes links to related components. The documentation assumes a general familiarity with C++, and particularly with C++ templates. The site also has a comprehensive introduction to STL that can be useful for programmers who are familiar with C++ but have never used STL. You can find the SGI STL pages at http://www.sgi.com/Technology/STL/ VISIT THE C/C++ USERS GROUP WEB SITE The C/C++ Users Group (CUG) is an independent organization that offers hundreds of classes, functions, and code libraries that you can download from the site. CUG focuses primarily on cross-platform compatibility with UNIX, Linux, Windows, DOS, and others. The code packages include algorithms, mathematical libraries, logging and reporting utilities, interpreters, compilers, file managers, networking tools, and more. To find the CUG Web site, go to http://www.hal9k.com/cug/ | |
A FUNCTION TEMPLATE CANNOT BE VIRTUAL | |
ADD A CATCH(...) BLOCK TO YOUR EXCEPTION HANDLERS HIERARCHY | |
APPLYING OPERATOR TYPEID TO NONPOLYMORPHIC OBJECTS | |
AUTO_PTR SHOULD NOT HOLD ARRAYS OF OBJECTS | |
AVOIDING RECURRENT POINTER DELETION | |
DEFINING A FUNCTION IN AN UNNAMED NAMESPACE | |
DYNAMIC BINDING AND STATIC BINDING | |
HELPING THE COMPILER DEDUCE THE TYPE OF A TEMPLATE ARGUMENT | |
INLINE OR __FORCEINLINE | |
MAKING A CLASS TEMPLATE A FRIEND OF ANOTHER CLASS TEMPLATE | |
PACK A LONG PARAMETER LIST IN A STRUCT | |
STRUCTURED EXCEPTION HANDLING VERSUS STANDARD EXCEPTION HANDLING | |
USING FUNDAMENTAL TYPES AS A TEMPLATE PARAMETER | |
DEFAULT PARAMETER VALUES ARE NOT PART OF A FUNCTION'S TYPE | |
FORWARD-DECLARING A NAMESPACE MEMBER | |
OPEN MODE FLAGS | |
THE WELL-FORMEDNESS OF ZERO SIZED ARRAYS | |
WHY YOU SHOULDN'T STORE AUTO_PTR OBJECTS IN STL CONTAINERS | |
CATCHING EXCEPTION IN A HIERARCHY | |
COPYING AND ASSIGNMENT OF CONTAINER OBJECTS | |
DECLARING REFERENCES TO FUNCTIONS | |
DESIGNING EXCEPTION CLASSES | |
DON'T CHANGE THE ACCESS OF A VIRTUAL MEMBER FUNCTION IN A DERIVED CLASS | |
FORWARD DECLARATIONS OF NESTED CLASSES | |
INITIALIZING A VECTOR WITH THE CONTENTS OF AN ARRAY | |
MEASURING SMALL TIME UNITS | |
MORE ON OPERATOR OVERLOADING | |
NAMESPACE ALIASES | |
OPTIMIZATIONS THAT ARE CARRIED OUT BY THE LINKER | |
THE ADDRESS OF A CLASS'S CONSTRUCTOR AND DESTRUCTOR | |
THE LIFETIME OF AN EXCEPTION OBJECT | |
DIFFERENCES BETWEEN VECTOR'S RESERVE() AND RESIZE() MEMBER FUNCTIONS | |
OPERATORS CANNOT BE OVERLOADED FOR USER-DEFINED TYPES | |
THE NAMED RETURN VALUE OPTIMIZATION | |
DENNIS RITCHIE'S HOME PAGE | |
DECLARING POINTERS TO DATA MEMBERS | |
INTRODUCING POINTERS TO MEMBERS | |
WHERE DEFAULT ARGUMENTS CAN APPEAR | |
BORLAND'S C++ 5.5 IS NOW GIVEN AWAY--FREE | |
CORRECTLY DECLARING GLOBAL OBJECTS | |
DECLARING A TYPEDEF | |
DECLARING POINTERS TO MEMBER FUNCTIONS | |
DON'T CONFUSE DELETE WITH DELETE[] | |
SOME DESIGN AND CODING RULE OF THUMBS | |
STATIC INITIALIZATION AND DYNAMIC INITIALIZATION | |
THE LINKAGE TYPE OF GLOBAL CONST OBJECTS | |
THE UNDERLYING REPRESENTATION OF POINTERS TO MEMBERS | |