C++ Resources, John Deacon |
||||||||||||
Traps and PitfallsThis began as an appendix to one of my C++ courses. I promised a couple of people that I would make it available online; the numbers are the page references for them (course notes version 2.5). It's also the first, and currently only, companion website resource for a book I am working on. This is a first draft (there is a change log below); I'm sure there are a few traps and pitfalls I've forgotten to put it; and the list will be revised; so by all means keep a bookmark or make a link, but please don't mirror the page. This isnt really a style guide; this document is rather too relentlessly and depressingly negative for that. Anyway, many, many excellent guides exist: one could start with the comp.lang.c++ FAQ, for example. However Traps and Pitfalls was the subtitle of the course from which it derives, and a summary was felt to be necessary. The decision as to what to include here usually involved surprise. If a particular point would show up as a compile error or would simply require effort to understand hairy syntax then it was not typically included. If something would compile but produce wrong or possibly unexpected results then it was included. Given that this began as an indexed appendix in some course notes, there is little explanation. (Framemaker's auto-appendix mechanism is brilliant but you're only allowed 255 characters.) The points are supposed to be aides memoire. If the points dont make enough sense, and neither does reading the notes, for those who have them, try the comp.lang.c++ newsgroup's FAQ. And, although its not ready yet, more explanation will eventually be available in the book I am working on. Obviously, there exists a wide spectrum of expertise in C++. These notes are meant for beginner- and intermediate-level C++ers; however, some of the more obvious traps have been marked as such. Ive tried to root out duplication; apologies for any that remain. Ive almost certainly fallen into a few traps myself here. Do feel free to email me. (A student once asked, Why do you teach C++ if you dislike it so? I dont dislike C++ at all; I think its great fun. (But them I am the kind of person who likes to construct ships in bottles, out of matchsticks.)) Object-orientation is just one of the styles supported by C++. Dont expect a great deal of help in adhering to any particular style. I-3 C++ is officially a general purpose programming language. While it offers support for object-orientation, it also offers support for structured, for generic, for mix-in and for no style at all. 1-3 C++ is officially based on C. C features like name hiding can interfere with C++ features like function overriding. 1-3 Just using classes doesnt make one object oriented. Classes also figure in generic C++ and mix-in C++. 1-3 Function name overloading is convenient and good when appropriate; but confusing if misused. Be careful that overloading and name hiding dont interfere. 1-3 References havent replaced pointers. Having both available makes it essential that one has a good conceptual model for differentiating them. 1-3 Primitive operands, arguments and returns get converted to their operators and functions expectations. 1-3 The presence of templates means that class member declarations involving a pointer to a template parameter might be interpreted as multiplication if you dont disambiguate with "typename". 1-9 Because the default for both objects and primitives is to pass to and from functions by copy, copy constructors are called more often than one might first imagine. 1-13 The special functions
will be used more than a sub-guru C++er would imagine. C++ makes copies
and temporaries under several circumstances. Special functions have even
more reason than usual to avoid side-effects and to be very careful about
throwing. C++ doesnt take the attitude that incorrect primitive operand or argument types should be compile errors; it assumes you meant what you wrote and want the operands and arguments to be converted. 1-15 [Fairly obvious] The compiler doesnt see your files; it sees the output of the preprocessor. 1-17 Some preprocessors are aged very aged. They might not understand // comments. Expect only that they understand /* */ comments. 1-19 Although you can start an identifier with an underscore, you shouldnt. Leading underscores are added to identifiers by C++ translators. 1-25 You might find bizarre things happening if you have an identifier like bitor or compl. They are alternative keyword forms of | and ~. 1-27 Tidying up columns of numbers with leading zeroes turns them into octal (base 8) numbers. 1-29 Its not an error to have two character symbols within the single quote marks of a char literal (constant). 1-29 The encodings for char and wchar_t are not defined. Dont assume ASCII or Unicode. 1-29 Literal floating-point values (constants) do not have type float; they have type double. 1-31 C++ is not one of those languages where, for example, one has functions (no side-effects) and procedures. In other words, what looks like an innocuous expression that computes a value, can cause non-local state changes. 2-6 Dividing by zero doesnt result in an exception. It results in undefined behavior. You must check your divisors yourself. 2-9 You are allowed to perform addition and subtraction on pointer values. This can result in sensible behavior or in undefined behavior. Its complicated. Read the text (and the standard). 2-11 Because assignment expressions are expressions and evaluate to values, and because values can frequently be converted to bool, accidentally using assignment where you meant comparison will almost always compile and fail silently at run-time 2-13 The order in which operands are evaluated is not defined. The fact that, for example, addition is defined to be left-associative, doesnt necessarily mean that the operands of the addition operator are evaluated from the left. 2-15 The operands of logical
and and logical or are evaluated from the left; however, there is a new
uncertainty: not all the operands need be evaluated at all. 2-15 E1 += E2, etc., are largely equivalent to E1 = E1 + E2, etc.; however, E1 is only evaluated once under +=, etc.. 2-17 Under assignment, when a pointer to an object is assigned another pointer to an object, they end up pointing at the same object. However, if an object-holding variable (or a reference to an object) is assigned another object, it becomes some kind of copy. 2-17 The increment (but not the decrement) operator can be applied to bool! It will set it to true whatever it was. 2-23 The precedence order of some operators including the assignment operators and the conditional (arithmetic if) was changed in ISO C++. 2-29 In general, blocks are good things. However, a declaration in a block within a block can hide a name from the outer block. And name hiding (an old feature) tends to interfere with clear OO (more recent features). 3-9 Whether you use curly brackets or not, selection and iteration statements control blocks. And the block begins at the opening parenthesis, not at the opening curly bracket (if there is one). 3-11 Selection and iteration
statements should be controlled by boolean expressions, but it is not
a compile error to put in numeric expression. 3-13 The = symbol is actually the assignment operator, so its all too easy to say "if I can assign b to a" rather than "if a is equal to b". In C++, because numeric results can be converted to bool, such mistakes usually compiles, to fail silently later. 3-15 If you dont put the curly brackets into an "if", it will be difficult to tell which "if" an "else" belongs to, if the "if" is, or ever becomes, nested. 3-17 The switch statement is really a slightly-tarted-up goto. This hints that maybe the switch should be avoided. 3-21 The switch statement involves just one block. The sections within are demarcated only with statement labels. You will need break statements after each (including the last for maintainers) section. 3-23 Accidentally drop one of the colons (:s) of a scope resolution operator, and you might just end up with a labeled statement. 3-26 A switch gets exponentially more difficult to maintain. One new type to be handled can involve updating many, many switch statements. Contrast this with adding a new class behind a polymorphic type. Prefer object-orientation over switches. 3-27 Many, many examples of the for statement have i++ to increment the loop counter. If i is an int, its not really significant, but theres always a chance that it might become an iterator, and then ++i is more efficient and here does just the same as i++. 3-33 The fors init-statement (the first thing inside the parentheses) is scoped to the fors block. In classic C++ it wasnt. 3-33 [Fairly obviously] C++ has a goto statement. You just might have led a very sheltered life and not learned that using the goto results in near-infinite coupling [a very bad thing for non-trivial systems]. 3-40 A C++ compiler isnt required to report an error if a value-returning function has no return statement. 3-41 (-PI <= theta <= PI) is always true, because the first comparison results in true or false which then convert to 0 or 1 which are both less than PI. In languages that dont convert boolean the expression is thankfully a compile error. 4-15 [So well known, its hardly a trap but] short, int and long could all be the same size on one machine, and need not be the same size on any other machine. If you want portable code, you dont use such built-in types; you use typedefs like size_t instead. 4-17 This is almost an anti-trap since consts are nearly always good. However, if you forget to make your query-only member functions const, then const objects of your class wont be able to accomplish much. 4-23 Applying delete to a pointer doesnt set it to zero; but you should probably consider doing so. 4-27 Primitive strings (C strings) are fraught with perils. Try not to use them. Consider objects of the string class instead. 4-35 However you declare the passing of a primitive string to a function, you will not pass a copy, but a pointer to the first character. 4-35 Primitive strings are one character bigger than you might think, since the end is indicated only by tacking on a zero-byte character. 4-35 Accidentally trashing the zero at the end of a primitive string sets up a disaster waiting to happen. 4-35 If you must use primitive strings, dont process them yourself; learn to use the string processing library functions properly and stick to them. 4-35 Primitive arrays are not bound-checked. 4-39 [Not really a pitfall; more of an easy mistake] The index number of the last element of an n element array is n-1. 4-43 Incorrectly accessing an array of arrays (when simulating a multi-dimensional array) as arr[i, j] instead of arr[i][j] is not a compile error even though it wont do what you probably expect. 4-43 References create aliases for identifiers. Be careful not to end up doing daft things like self-assignment. Self assignment might go horribly wrong for classes with badly-written copy assignment operators. 4-45 [To avoid setting up your own mental traps] Keep repeating, "Anything I appear to be doing to a reference, I am actually doing to the thing its referencing." 4-46 While a reference might be a pointer behind the scenes, 95% of the time you shouldnt think of it as a pointer. Dont try to check for null references, for example; such things cant exist. 4-47 Do not return via references unless you are absolutely sure that you know exactly what you are doing. (And make sure you know if and how your compiler supports the return value optimization.) 4-53 Dont try to be tidy by including empty parentheses when initializing object in variables via the default constructor. You will actually declaring parameterless functions instead. 4-65 Beware a course or book that tells you that classes are for making objects. Its much more complex than that classes are used for three purposes in OO C++, and could be used for at least another three outside of that. 5-3 Class definitions (typical contents of object-oriented header files) are declaration statements and must end in ";"s Forgetting this usually leads to an impenetrable error message in another file. 5-19 Dont think of public as meaning the same for data and function members. Public data members can be accessed; but public member functions cant be called. Public member functions define the signatures of messages that all other objects can send. 5-27 Unlike, say, Smalltalk,
C++ has class-level encapsulation. An object isnt prevented from
accessing the private data members of another instance of its class. C++ doesnt enforce the private access category for data members. One day youll be tempted to make a data member of a base class protected. Resist. Dont do it. From that moment on, the base class becomes unmaintainable. 5-33 If you think about it, it would have to be a very clever compiler that could apply the inline optimization to a function that hadnt been #included. So for most situations today, the inline keyword in a .cpp (.cxx) file is a waste of time. 5-42 Its not mandatory to give formal parameter names in header files, so its easy to have parameter declarations that are not self-documenting. 5-45 Object instance arguments are passed by value, i.e. by copying. But forget to provide the copy constructor and one will be provided for you. And Finagles Law says that anytime you do forget, the implicit copy constructors behavior wont be right. 5-47 Arrays (like char[], mentioned earlier) are not passed to functions by the normal, by-value, mechanism. Instead, a pointer to the first element is passed. 5-49 Forgotten parentheses on an argumentless message or call will often compile because a functions address is a valid numeric expression. 5-51 Const objects can change. While passing out a pointer (or reference) to a data member is hardly a subtle trap, data members can be declared mutable however; so we hope that such data member have been designed to be undetectable from outside their object. 5-57 Static data members are close to global. Static member function uses are close to calls (rather than messages). Overusing static members takes one away from object-orientation and towards the less useful class-orientation. 5-59 Its possible to create systems where it becomes almost impossible to say which function will be used. Strive to avoid mixing the possibilities of overloading, argument conversions, templates and default parameter values. 5-67 Never mix up the two initialization syntax schemes of "=" and "()". Try to always use ()-style initialization. Never, ever allow the initialization of an object of class type to involve = and () at the same time. 5-68 Objects get initialized, which is good. It does mean, however, that an array of n objects isnt an array with space for n objects, its an array with n default constructed objects already there. 6-9 The C memory mechanisms malloc and free still exist in C++, of course. Its not a compile mistake to free() something newed or to delete something malloc()ed; just lethal. 6-15 Its not a compile error to "delete" (rather than "delete[]") something that was "new []"ed. Its almost certainly a memory problem though. 6-17 [So famous its hardly a trap] The C++ system doesnt reclaim unreferenced free store (doesnt do "garbage collection"). The programmer must return free store memory when its no longer required. Copied pointers make this a very challenging aspect of C++. 6-21 To realize how quickly it could become impossible to decide who takes out the garbage, one only has to recall that the default argument passing and return mechanisms are by-copy; and that the implicit copy constructor does shallow copy. 6-21 Looking for smart pointers to help with garbage collection and with polymorphic collections, one turns to the library. But the smart pointer one finds there auto_ptr mustnt be used for either of those purposes. 7-9 Dont pass an auto_ptr to a function by value. The auto_ptr will be copied and the copy will become the owner When the function finishes, the local copied auto_ptr goes out of scope and it will delete what it was pointing at. 7-9 Almost uniquely among languages today, C++ allows one to store entire objects in variables. This leads to no end of complications; and certainly leads to the special functions being special. 8-3 A class without constructors may well have implicit constructors. There are only two circumstance when an implicit copy constructor wouldnt be provided; and only a few more where a default constructor wouldnt. And the implicit ones are public. 8-11 People sometimes try to call one constructor from another. Dont. You can write something that looks like it might just be a call to a constructor but it wont be. Constructors are always declaration statement stuff rather than expression statement stuff. 8-14 A one-argument constructor will be implicitly used as a conversion function (argument type to class type) unless you say not to with the "explicit" keyword. And constructors with default parameter values might be also be considered single-argument. 8-15 Another reason not to use the = sign in declaration/initialization is to avoid encouraging the mistaken belief that the copy assignment operator would be used. It wouldnt be. The copy constructor will be used for both = style and () style initialization. 8-18 The implicit copy constructor effectively does shallow copy it copies pointers but not pointees. 8-19 Lulled into complacency by constructor chaining, you might imagine that the copy assignment operator automatically chains as well. It doesnt. You will probably want to though. 8-26 [Widely known but] Primitives, including pointers, have random initial values for all practical purposes (since we wont be using static storage much, will we). 8-31 Data members holding objects of class type are default constructed before the constructors code block runs. Initialize them via the member initialization list instead. 8-33 If you initialize
an object of class type via {} expression lists, almost any change to
the class will invalidate such an initialization. 8-37 Although destructors can be called explicitly, it is an exotic and probably dangerous thing to do. Be happy with automatic calling of destructors. 8-45 The implicit destructor is non-polymorphic (non-virtual). Just about everyone, just about all the time must consider whether their destructor should be polymorphic (virtual); and probably conclude that it should be. 8-49 You might not think you need a destructor, but unless you can prove that your class could never be a base class, you should provide a virtual (not pure virtual) destructor, even it is has an empty code block. 8-51 Overloaded operators must keep the precedence of the built-in operator. So trying to use ^ for an exponentiation operator will give counter-intuitive precedence, i.e. will always require parenthetical disambiguation. 9-7 Remember that neither the number of arguments nor their position contribute to the "name" of a member function. So an overload of unary +, say, in a derived class would hide a binary + in a base class. 9-8 Operator overloads are inherited but the implicit copy assignment operator usually hides any base class copy assignment operator. 9-13 You cant make && or || (or ,) operator overloads work intuitively, so dont provide them. (You cant mimic guaranteed operator evaluation order or lazy evaluation with member functions.) 9-29 You might be tempted to provide operator bool in order that your objects can easily be tested for validity. But beware that once your objects can be converted to bool, they can also be converted to int. 9-34 [Well known but] Not only is int converted to long, for example, but long is converted to int, and float is even converted to int. If youre lucky, a friendly compiler will issue a warning. So treat C++ compiler warnings as though they were errors. 10-11 Inheritance of implementation has not turned out to be the labor-saving device we thought it would be. (Unlike composition, it doesnt function through low-coupling, program-by-contract messages.) 11-3 While the maintainability of the derived classes goes up because of factoring, the maintainability of the base class rapidly goes down. So be sure your code is stable before you start introducing inheritance of implementation. 11-3 Given the fragility of base classes, they should always be destined and designed to be base classes. A class should never just slip into becoming a base class. Design your concrete, derived classes so that they could never become base classes. 11-3 So why dont we ban inheritance? Because theres something more important than implementation to inherit. Public inheritance gives objects extra types; it supports polymorphism. 11-5 Private data members are present in the instances of derived classes. They just cant be accessed by derived class code (which is a good thing). 11-7 Beware style guides
that say multiple inheritance mustnt be used. Its OK for an
object to inherit multiple types, but its way more trouble than
its worth for an object to inherit implementation from more than
one base class. The class keyword brings private access for the members. But that includes inheritance; and private inheritance is not the object-oriented way. Private inheritance is the (now not very popular) mixin way. 11-11 Dont be trapped into mentioning base class names any more than you have to. Use the "typedef BaseClass super" trick to avoid it. And dont use any class name as part of a function name. 11-20 [As mentioned earlier] Beware if you think youre overloading a member function from a base class. A derived class member function with the same name as a base class member function name-hides the base class one. A using declaration can sort this out. 11-21 Unless you use virtual member functions, C++ doesnt give you polymorphic behavior. By default C++ uses the pointer type to select member functions. That way lies redundant overriding and inconsistent binding. 11-23 Dont be misled into thinking that theres anything "ghostly" or "not really there" about virtual member functions. Pronounce "virtual" as "polymorphic". (The ghostly ones are the pure virtuals.) 11-25 Its not a compile error to forget to pass a derived class object by pointer or reference, when the parameter declaration was for a base class object. What will happen is that the derived bits will be "sliced" off as the argument is copied onto the stack. 11-26 Although making a
member function polymorphic (virtual) means that the object selects the
member function, its still the pointer type that selects the access
category. Dont be misled by the unforgivably obscure syntax (= 0) into thinking that pure virtual member functions are some dark and dusty corner that is to be ignored. They are pivotal to good OO. 11-37 Dont imagine that a class with no code and no data members is worthless. A class with nothing but pure virtual member functions (a pABC) is an excellent and simple type that classes can implement. 11-39 Dont by misled by some books (or even libraries and frameworks) into thinking its OK to instantiate base classes. Its almost never a good idea you end up with a class doing three jobs, and two is bad enough. 11-39 If you disregard the earlier advice to allow one or none of your base classes to have implementation, you will also and always have to consider whether your inheritance should be virtual inheritance. 11-45 Dont imagine
that because the RTTI (run-time type identification) was introduced,
you should use it. Treat "Whats your class?" as a rude
question and use it only once in a blue moon. Its not an afterthought (I hope) that the type_info object supports one in avoiding mentioning a class by name. Avoid building class names into anything but declarations. And even in declarations avoid concrete class names as types. 12-5 Putting "using namespace std" makes a very large set of unseen names available to clash in "what on earths going on" kinds of ways with your identifiers - particularly at some point in the future. 14-13 There might be more namespaces than you thought in which C++ will look for a function. C++ also searches for a function in the namespaces in which any of a functions arguments of class type are defined. 14-21 If any function you call (or that it calls, or ...) could throw, you need to be an order of magnitude more careful a programmer than you would otherwise have needed. 15-11 A derived object catcher that follows a catcher for its base class can never be reached. 15-17 Although you dont have to catch exception objects by reference, getting into the habit of doing anything else is asking for trouble. See bit slicing. 15-18 Exception specifications are not checked at compile time. Java programmers beware. A function with an exception specification that tries to emit an undeclared exception at run time will cause the program to terminate. 15-19 Change log (ISO date format -- yyyy-mm-dd) 2005-03-31: first draft made available. |