![]() | CONTENTS | ![]() |
You might have programs (or programming habits) developed in C or in older versions of C++ that you want to convert to Standard C++. This appendix provides some guidelines. Some pertain to moving from C to C++, others from older C++ to Standard C++.
The C/C++ preprocessor provides an array of directives. In general, C++ practice is to use those directives designed to manage the compilation process and to avoid using directives as a substitute for code. For example, the #include directive is an essential component for managing program files. Other directives, such as #ifndef and #endif, let you control whether particular blocks of code get compiled. The #pragma directive lets you control compiler-specific compilation options. These all are useful, sometimes necessary, tools. You should exert caution, however, when it comes to the #define directive.
Symbolic constants make code more readable and maintainable. The constant's name indicates its meaning, and if you need to change the value, you just have to change the value once, in the definition, then recompile. C used the preprocessor for this purpose:
#define MAX_LENGTH 100
The preprocessor then does a text substitution in your source code, replacing occurrences of MAX_LENGTH with 100 prior to compilation.
The C++ approach is to apply the const modifier to a variable declaration:
const int MAX_LENGTH = 100;
This treats MAX_LENGTH as a read-only int.
There are several advantages to the const approach. First, the declaration explicitly names the type. For #define, you must use various suffixes to a number to indicate types other than char, int, or double; for example, using 100L to indicate a long type or 3.14F to indicate a float type. More importantly, the const approach can just as easily be used with derived types:
const int base_vals[5] = {1000, 2000, 3500, 6000, 10000}; const string ans[3] = {"yes", "no", "maybe"};
Finally, const identifiers obey the same scope rules as variables. Thus, you can create constants with global scope, named namespace scope, and block scope. If, say, you define a constant in a particular function, you don't have to worry about the definition conflicting with a global constant used elsewhere in a program. For example, consider the following:
#define n 5 const int dz = 12; ... void fizzle() { int n; int dz; ... }
The preprocessor will replace
int n;
with
int 5;
and induce a compilation error. The dz defined in fizzle(), however, will be a local variable. Also, fizzle(), if necessary, can use the scope resolution operator and access the constant as ::dz.
C has borrowed the const keyword from C++, but the C++ version is more useful. For example, the C++ version has internal linkage for external const values rather than the default external linkage used by variables and by the C const. This means that each file in a program using a const needs that const defined in that particular file. This might sound like extra work, but, in fact, it makes life easier. With internal linkage, you can place const definitions in a header file used by various files in a project. That is a compiler error for external linkage but not for internal linkage. Also, because a const must be defined in the file using it (being in a header file used by that file satisfies the requirement), you can use const values as array size arguments:
const int MAX_LENGTH = 100; ... double loads[MAX_LENGTH]; for (int i = 0; i < MAX_LENGTH; i++) loads[i] = 50;
This won't work in C because the defining declaration for MAX_LENGTH could be in a separate file and not be available when this particular file is compiled. In fairness, it should be added that, in C, you could use the static modifier to create constants with internal linkage. It's just that C++, by making static the default, requires one less thing for you to remember.
Incidentally, the revised C standard (C99) does allow you to use a const as an array size, but the array is treated as a new form of array, called a variable array, that is not part of the C++ standard.
The #define directive, however, still is useful as part of the standard idiom for controlling when a header file is compiled:
// blooper.h #ifndef _BLOOPER_H_ #define _BLOOPER_H_ // code goes here #endif
For typical symbolic constants, however, get into the habit of using const instead of #define. Another good alternative, particularly when you have a set of related integer constants, is to use enum:
enum {LEVEL1 = 1, LEVEL2 = 2, LEVEL3 = 4, LEVEL4 = 8};
The traditional C way to create the near-equivalent of an inline function was to use a #define macro definition:
#define Cube(X) X*X*X
This lead the preprocessor to do text substitution, with X being replaced by the corresponding argument to Cube():
y = Cube(x); // replaced with y = x*x*x; y = Cube(x + z++); // replaced with x + z++*x + z++*x + z++;
Because the preprocessor uses text substitution instead of true passing of arguments, using such macros can lead to unexpected and incorrect results. Such error can be reduced by using lots of parentheses in the macro to ensure the correct order of operations:
#define Cube(X) ((X)*(X)*(X))
Even this, however, doesn't deal with cases such as using values like z++.
The C++ approach of using the keyword inline to identify inline functions is much more dependable because it uses true argument passing. Furthermore, C++ inline functions can be regular functions or class methods.
One positive feature of the #define macro is that it is typeless so it can be used with any type for which the operation makes sense. In C++ you can create inline templates to achieve type-independent functions while retaining argument passing.
In short, use C++ inlining instead of C #define macros.
Actually, you don't have a choice. Although prototyping is optional in C, it is mandatory in C++. Note that a function that is defined before its first use, such as an inline function, serves as its own prototype.
Do use const in function prototypes and headers when appropriate. In particular, use const with pointer parameters and reference parameters representing data that is not to be altered. Not only does this allow the compiler to catch errors that change data, it also makes a function more general. That is, a function with a const pointer or reference can process both const and non-const data, while a function that fails to use const with a pointer or reference only can process non-const data.
One of Stroustrup's pet peeves about C is its undisciplined type cast operator. True, type casts often are necessary, but the standard type cast is too unrestrictive. For example, consider the following code:
struct Doof { double feeb; double steeb; char sgif[10]; }; Doof leam; short * ps = (short *) & leam; // old syntax int * pi = int * (&leam); // new syntax
Nothing in the language prevents you from casting a pointer of one type to a pointer to a totally unrelated type.
In a way, the situation is similar to that of the goto statement. The problem with the goto statement was that it was too flexible, leading to twisted code. The solution was to provide more limited, structured versions of goto to handle the most common tasks for which goto was needed. This was the genesis of language elements such as for and while loops and if else statements. Standard C++ provides a similar solution for the problem of the undisciplined type cast, namely, restricted type casts to handle the most common situations requiring type casts. These are the type cast operators discussed in Chapter 15, "Friends, Exceptions, and More":
dynamic_cast static_cast const_cast reinterpret_cast
So, if you are doing a type cast involving pointers, use one of these operators if possible. Doing so both documents the intent of the cast and provides checking that the cast is being used as intended.
If you've been using malloc() and free(), switch to using new and delete instead. If you've been using setjmp() and longjmp() for error handling, use try, throw, and catch instead. Try using the bool type for values representing true and false.
The Standard specifies new names for the header files, as described in Chapter 2, "Setting Out to C++." If you've been using the old-style header files, you should change over to using the new-style names. This is not just a cosmetic change because the new versions might add new features. For example, the ostream header file provides support for wide-character input and output. It also provides new manipulators such as boolalpha and fixed (as described in Chapter 17, "Input, Output, and Files"). These offer a simpler interface than using setf() or the iomanip functions for setting many formatting options. If you do use setf(), use ios_base instead of ios when specifying constants; that is, use ios_base::fixed instead of ios::fixed. Also, the new header files incorporate namespaces.
Namespaces help organize identifiers used in a program in order to avoid name conflicts. Because the standard library, as implemented with the new header file organization, places names in the std namespace, using these header files requires that you deal with namespaces.
The examples in this book, for simplicity, utilize a using directive to make all the names from the std namespace available:
#include <iostream> #include <string> #include <vector> using namespace std; // a using-directive
However, the wholesale exporting of all the names in a namespace, whether needed or not, runs counter to the goals of namespaces.
Instead, the recommended approach is to use either using declarations or the scope resolution operator (::) to make available just those names a program needs. For example,
#include <iostream> using std::cin; // a using-declaration using std::cout; using std::endl;
makes cin, cout, and endl available for the rest of the file. Using the scope resolution operator, however, makes a name available just in the expression using the operator:
cout << std::fixed << x << endl; //using the scope resolution operator
This could get wearisome, but you can collect your common using declarations in a header file:
// mynames -- a header file #include <iostream> using std::cin; // a using-declaration using std::cout; using std::endl;
Going a step further, you could collect using declarations in namespaces:
// mynames -- a header file #include <iostream> namespace io { using std::cin; using std::cout; using std::endl; } namespace formats { using std::fixed; ....using std::scientific; using std:boolalpha; }
Then a program could include this file and use the namespaces it needs:
#include "mynames" using namespace io;
Each use of new should be paired with a use of delete. This can lead to problems if a function in which new is used terminates early via an exception being thrown. As discussed in Chapter 15, using an autoptr object to keep track of an object created by new automates the activation of delete.
The traditional C-style string suffers from not being a real type. You can store a string in a character array, you can initialize a character array to a string. But you can't use the assignment operator to assign a string to a character array; instead, you must remember to use strcpy() or strncpy(). You can't use the relational operators to compare C-style strings; instead, you must remember to use strcmp(). (And if you forget and use, say, the > operator, you don't get a syntax error; instead, the program compares string addresses instead of string contents.)
The string class (Chapter 16, "The string Class and the Standard Template Library," and Appendix F, "The string Template Class"), on the other hand, lets you use objects to represent strings. Assignment, relational operators, and the addition operator (for concatenation) all are defined. Furthermore, the string class provides automatic memory management so that you normally don't have to worry about someone entering a string that either overruns an array or gets truncated before being stored.
The string class provides many convenience methods. For example, you can append one string object to another, but you also can append a C-style string or even a char value to a string object. For functions that require a C-style string argument, you can use the c_str() method to return a suitable pointer-to-char.
Not only does the string class provide a well-designed set of methods for handling string-related tasks, such as finding substrings, but it also features a design that is compatible with the STL so that you can use STL algorithms with string objects.
The Standard Template Library (Chapter 16 and Appendix G, "The STL Methods and Functions") provides ready-made solutions to many programming needs, so use it. For example, instead of declaring an array of double or of string objects, you can create a vector<double> object or a vector<string> object. The advantages are similar to those of using string objects instead of C-style strings. Assignment is defined, so you can use the assignment operator to assign one vector object to another. You can pass a vector object by reference, and a function receiving such an object can use the size() method to determine the number of elements in the vector object. Built-in memory management allows for automatic resizing when you use the pushback() method to add elements to a vector object. And, of course, several useful class methods and general algorithms are at your service.
If you need a list, a double-ended queue (or deque), a stack, a regular queue, a set, or a map, the STL provides useful container templates. The algorithm library is designed so that you easily copy the contents of a vector to a list or compare the contents of a set to a vector. This design makes the STL into a toolkit providing basic units that you can assemble as needed.
The extensive algorithm library was designed with efficiency as one of the main design goals, so you can get top-flight results with relatively little programming effort on your part. And the iterator concept used to implement the algorithms means that they aren't limited to being used with STL containers. In particular, they can be applied to traditional arrays, too.
![]() | CONTENTS | ![]() |