[NOTE] More Effective C++
CH1: Basics
Item 1: Distinguish between pointers and references
-
A reference must always refer to some object. The fact that there is no such thing as a null reference implies that it can be more efficient to use references than to use pointers.
-
A pointer may be reassigned to refer to different objects. A reference, however, always refers to the object with which it is initialized.
-
When you’re implementing certain operators, there are some situations in which you should use a reference. The most common example is
operator[]
.1// Undefined behavior 2char* pc = 0; 3char& rc = *pc; 4 5// No need to check if it is a null reference 6void printDouble(const double& rd) { 7 std::cout << rd << std::endl; 8} 9 10void printDouble(const double* pd) { 11 if (pd) { 12 std::cout << *pd << std::endl; 13 } 14}
Item 2: Prefer C++-style casts
-
static_cast
has basically the same power and meaning as the general-purpose C-style cast. -
const_cast
is used to cast away theconst
ness orvolatile
ness of an expression. -
dynamic_cast
is used to perform safe casts down or across an inheritance hierarchy.- Failed casts are indicated by a null pointer (when casting pointers) or an exception (when casting references).
- They cannot be applied to types lacking virtual functions, nor can they cast away
const
ness.
-
reinterpret_cast
is used to perform type conversions whose result is nearly always implementation-defined.1// Example: static_cast 2int first_number = 1, second_number = 1; 3double result = static_cast<double>(first_number) / second_number; 4 5// Example: const_cast 6const int val = 10; 7const int *const_ptr = &val; 8int *nonconst_ptr = const_cast<int *>(const_ptr); 9 10// Example: dynamic_cast 11class Base { 12 virtual void DoIt() { std::cout << "This is Base" << std::endl; } 13}; 14class Derived : public Base { 15 virtual void DoIt() { std::cout << "This is Derived" << std::endl; } 16}; 17Base *b = new Derived(); 18Derived *d = dynamic_cast<Derived *>(b); 19 20// Example: reinterpret_cast 21int *a = new int(); 22void *b = reinterpret_cast<void *>(a); // the value of b is unspecified 23int *c = reinterpret_cast<int *>(b); // a and c contain the same value
Item 3: Never treat arrays polymorphically
-
The language specification says the result of deleting an array of derived class objects through a base class pointer is undefined.
1class BST { 2 friend std::ostream& operator<<(std::ostream& s, const BST& data); 3}; 4class BalancedBST : public BST {}; 5 6// Note: array[i] is really just shorthand for an expression involving pointer 7// arithmetic, and it stands for *(array+i) 8void printBSTArray(std::ostream& s, const BST array[], int numElements) { 9 for (int i = 0; i < numElements; ++i) { 10 s << array[i]; 11 } 12} 13 14BalancedBST bBSTArray[10]; 15// They'd assume each object in the array is the size of BST, but each object 16// would actually be the size of a BalancedBST 17printBSTArray(std::cout, bBSTArray, 10);
Item 4: Avoid gratuitous default constructors
-
If a class lacks a default constructor, its use may be problematic in three contexts.
- The creation of arrays.
- There’s no way to specify constructor arguments for objects in arrays.
- Ineligible for use with many template-based container classes.
- It’s common requirement for such templates that the type used to instantiate the template provide a default constructor.
- Virtual base classes lacking default constructors are a pain to work with.
- The arguments for virtual base class constructors must be provided by the most derived class of the object being constructed.
1// Example: the creation of arrays 2class EquipmentPiece { 3 public: 4 EquipmentPiece(int IDNumber); 5 // ... 6}; 7 8// Solution 1 9// Provide the necessary arguments at the point where the array is defined 10int ID1, ID2, ID3, ..., ID10; 11// ... 12EquipmentPiece bestPieces[] = {ID1, ID2, ID3, ..., ID10}; 13 14// Solution 2 15// Use an array of pointers instead of an array of objects 16typedef EquipmentPiece* PEP; 17PEP bestPieces[10]; // on the stack 18PEP* bestPieces = new PEP[10]; // on the heap 19for (int i = 0; i < 10; ++i) { 20 bestPieces[i] = new EquipmentPiece(/* ID Number */); 21} 22 23// Solution 3 24// Allocate the raw memory for the array, then use "placement new" to construct 25void* rawMemory = operator new[](10 * sizeof(EquipmentPiece)); 26EquipmentPiece* bestPieces = static_cast<EquipmentPiece*>(rawMemory); 27for (int i = 0; i < 10; ++i) { 28 new (&bestPieces[i]) EquipmentPiece(/* ID Number */); 29} 30for (int i = 9; i >= 0; --i) { 31 bestPieces[i].~EquipmentPiece(); 32} 33operator delete[] bestPieces;
- The creation of arrays.
CH2: Operators
Item 5: Be wary of user-defined conversion functions
-
Two kinds of functions allow compilers to perform implicit type conversions.
- A single-argument constructor is a constructor that may be called with only one argument.
- An implicit type conversion operator is simply a member function with a strange-looking name: the word
operator
followed by a type specification.
-
Constructors can be declared
explicit
, and if they are, compilers are prohibited from invoking them for purposes of implicit type conversion. -
Proxy objects can give you control over aspects f your software’s behavior, in this case implicit type conversions, that is otherwise beyond our grasp.
- No sequence of conversions is allowed to contain more than one user-defined conversion.
1// The single-argument constructors 2class Name { 3 public: 4 Name(const std::string& s); 5 // ... 6}; 7 8// The implicit type conversion operators 9class Rational { 10 public: 11 operator double() const; 12}; 13 14// Usage of `explicit` 15template <class T> 16class Array { 17 public: 18 // ... 19 explicit Array(int size); 20 // ... 21}; 22 23// Usage of proxy classes 24template <class T> 25class Array { 26 public: 27 class ArraySize { // this class is new 28 public: 29 ArraySize(int numElements) : theSize(numElements) {} 30 int size() const { return theSize; } 31 32 private: 33 int theSize; 34 }; 35 36 Array(int lowBound, int highBound); 37 Array(ArraySize size); // note new declaration 38};
Item 6: Distinguish between prefix and postfix forms of increment and decrement operators
-
The prefix forms return a reference, while the post forms return a
const
object. -
While dealing with user-defined types, prefix increment should be used whenever possible, because it’s inherently more efficient.
-
Postfix increment and decrement should be implemented in terms of their prefix counterparts.
1class UPInt { 2 public: 3 UPInt& operator++(); // prefix ++ 4 const UPInt operator++(int); // postfix ++ 5 UPInt& operator--(); // prefix -- 6 const UPInt operator--(int); // postfix -- 7 UPInt& operator+=(int); // a += operator for UPInts and ints 8}; 9 10UPInt& UPInt::operator++() { 11 *this += 1; 12 return *this; 13} 14 15const UPInt UPInt::operator++(int) { 16 UPInt oldValue = *this; 17 ++(*this); 18 return oldValue; 19}
Item 7: Never overload &&
, ||
, or ,
- C++ employs short-circuit evaluation of boolean expressions, but function call semantics differ from short-circuit semantics in two crucial ways.
- When a function call is made, all parameters must be evaluated, so when calling the function
operators&&
andoperator||
, both parameters are evaluated. - The language specification leaves undefined the order of evaluation of parameters to a function call, so there is no way of knowing whether
expression1
orexpression2
well be evaluated first.
- When a function call is made, all parameters must be evaluated, so when calling the function
- An expression containing a comma is evaluated by first evaluating the part of the expression to the left of the comma, then evaluating the expression to the right of the comma; the result of the overall comma expression is the value of the expression on the right.
Item 8: Understand the different meanings of new
and delete
-
The
new
you are using is thenew
operator.- First, it allocates enough memory to hold an object of the type requested. The name of the function the
new
operator calls to allocate memory isoperator new
. - Second, it calls a constructor to initialize an object in the memory that was allocated.
- A special version of
operator new
called placementnew
allows you to call a constructor directly.
- First, it allocates enough memory to hold an object of the type requested. The name of the function the
-
The function
operator delete
is to the built-indelete
operator asoperator new
is to thenew
operator. -
There is only one global
operator new
, so if you decide to claim it as your own, you instantly render your software incompatible with any library that makes the same decision.1// `new` operator: 2// Step 1: void *memory = operator new(sizeof(std::string)) 3// Step 2: call std::string::string("Memory Management") on *memory 4// Step 3: std::string *ps = static_cast<std::string*>(memory) 5// 6// `delete` operator: 7// Step 1: ps->~string() 8// Step 2: operator delete(ps) 9 10// Placement `new`: 11class Widget { 12 public: 13 Widget(int widgetSize); 14 // ... 15}; 16// It's just a use of the `new` operator with an additional argument 17// (buffer) is being specified for the implicit call that the `new` 18// operator makes to `operator new` 19Widget* constructWidgetInBuffer(void* buffer, int widgetSize) { 20 return new (buffer) Widget(widgetSize); 21}
CH3: Exceptions
Item 9: Use destructors to prevent resource leaks
- By adhering to the rule that resources should be encapsulated inside objects, you can usually avoid resource leaks in the presence of exceptions.
Item 10: Prevent resource leaks in constructors
-
C++ destroys only fully constructed objects, and an object isn’t fully constructed until its constructor has return to completion.
-
If you replace pointer class members with their corresponding
auto_ptr
objects, you fortify your constructors against resource leaks in the presence of exceptions, you eliminate the need to manually deallocate resources in destructors, and you allowconst
member pointers to be handled in the same graceful fashion as non-const
pointers.1class BookEntry; 2 3// If BookEntry's constructor throws an exception, pb will be the null pointer, 4// so deleting it in the catch block does nothing except make you feel better 5// about yourself 6void testBookEntryClass() { 7 BookEntry* pb = 0; 8 try { 9 pb = new BookEntry(/* ... */); 10 // ... 11 } catch (/* ... */) { 12 delete pb; 13 throw; 14 } 15 delete pb; 16}
Item 11: Prevent exceptions from leaving destructors
-
You must write your destructors under the conservative assumption that an exception is active, because if an exception is thrown while another is active, C++ calls the
terminate
function. -
If an exception is throw from a destructor and is not caught there, that destructor won’t run to completion.
-
There are two good reasons for keeping exceptions from propagating out of destructors.
- It prevents
terminate
from being called during the stack-unwinding part of exception propagation. - It helps ensure that desturctors always accomplish everything they are supposed to accomplish.
1class MyClass { 2 public: 3 void doSomeWork(); 4 // ... 5}; 6 7try { 8 MyClass obj; 9 // If an exception is thrown from doSomeWork, before execution moves out from the try block, 10 // the destructor of obj needs to be called as obj is a properly constructed object 11 // What if an exception is also thrown from the destructor of obj? 12 obj.doSomeWork(); 13 // ... 14} catch (std::exception& e) { 15 // do error handling 16}
- It prevents
Item 12: Understand how throwing an exception differs from passing a parameter or calling a virtual function
-
Exception objects are always copied; when caught by value, they are copied twice. Objects passed to function parameters need not be copied at all.
- When an object is copied for use as an exception, the copying is performed by the object’s copy constructor. This copy constructor is the one in the class corresponding to the object’s static type, not its dynamic type.
- Passing a temporary object to a non-
const
reference parameter is not allowed for function calls, but it is for exceptions.
-
Objects thrown as exceptions are subject to fewer forms of type conversion than are objects passed to functions.
- A
catch
clause for base class exceptions is allowed to handle exceptions of derived class types. - From a typed to an untyped pointer.
- A
-
catch
clauses are examined in the order in which they appear in the source ode, and the first one that can succeed is selected for execution.- Never put a
catch
clause for a base class before acatch
clause for a derived class.
1// Difference 1 2class Widget {}; 3class SpecialWidget : public Widget {}; 4 5void passAndThrowWidget() { 6 SpecialWidget localSpecialWidget; 7 // ... 8 Widget& rw = localSpecialWidget; // rw refers to a SpecialWidget 9 throw rw; // this throws an exception of type Widget 10} 11 12try { 13 passAndThrowWidget(); 14} 15catch (Widget& w) { // catch Widget exceptions 16 // ... // handle the exception 17 throw; // rethrow the exception so it continues to propagate 18} catch (Widget& w) { // catch Widget exceptions 19 // ... // handle the exception 20 throw w; // propagate a copy of the caught exception 21} 22 23// Difference 2 24// Can catch errors of type runtime_error, range_error, or overflow_error 25catch (std::runtime_error); 26catch (std::runtime_error&); 27catch (const std::runtime_error&); 28// Can catch any exception that's a pointer 29catch(const void*); 30 31// Difference 3 32try { 33 // ... 34} catch (std::invalid_argument& ex) { 35 // ... 36} catch (std::logic_error& ex) { 37 // ... 38}
- Never put a
Item 13: Catch exception specifications judiciously
-
If you try to catch exceptions by pointer, you must define exception objects in a way that guarantees the objects exist after control leaves the functions throwing pointers to them. Global and static objects work fine, but it’s easy for you to forget the constraint.
- The four standard exceptions,
bad_alloc
,bad_cast
,bad_typeid
, andbad_exception
are all objects, not pointers to objects.
- The four standard exceptions,
-
If you try to catch exceptions by value, it requires that exception objects be copied twice each time they’re thrown and also gives rise to the specter of the slicing problem.
-
If you try to catch exceptions by reference, you sidestep questions about object deletion that leave you demand if you do and damned if you don’t; you avoid slicing exception objects; you retain the ability to catch standard exceptions; and you limit the number of times exception objects need to be copied.
Item 14: Use exception specifications judiciously
-
The default behavior for
unexpected
is to callterminate
, and the default behavior forterminate
is to callabort
, so the default behavior for a program with a violated exception specification is to halt. -
Compilers only partially check exception usage for consistency with exception specifications. What they do not check for is a call to a function that might violate the exception specification of the function making the call.
-
Avoid putting exception specifications on templates that take type arguments.
-
Omit exception specifications on functions making calls to functions that themselves lack exception specifications.
- One case that is easy to forget is when allowing users to register callback functions.
-
Handle exceptions “the system” may throw.
-
If preventing exceptions isn’t practical, you can exploit the fact that C++ allows you to replace unexpected exceptions with exceptions of a different type.
-
Exception specifications result in
unexpected
being invoked even when a higher-level caller is prepared to cope with the exception that’s arisen.1// Partially check 2extern void f1(); 3void f2() throw(int) { 4 // ... 5 f1(); // legal even though f1 might throw something besides an int 6 // ... 7} 8 9// A poorly designed template wrt exception specifications 10// Overloaded operator& may throw an exception 11template <class T> 12bool operator==(const T& lhs, const T& rhs) throw() { 13 return &lhs == &rhs; 14} 15 16// Replace the default unexpected function 17class UnexpectedException { 18}; // all unexpected exception objects will be replaced by objects of this type 19void convertUnexpected() { 20 throw UnexpectedException(); 21} // function to call if an unexpected exception is thrown 22std::set_unexpected(convertUnexpected); 23 24// Suppose some function called by logDestruction throws an exception 25// When this unanticipated exception propagates through logDestruction, unexpected 26// will be called 27class Session { 28 public: 29 ~Session(); 30 // ... 31 private: 32 static void logDestruction(Session* objAddr) throw(); 33}; 34 35Session::~Session() { 36 try { 37 logDestruction(this); 38 } catch (/* ... */) { 39 // ... 40 } 41}
Item 15: Understand the costs of exception handling
- To minimize your exception-related costs, compile without support for exceptions when that is feasible; limit your use of
try
blocks and exception specifications to those locations where you honestly need them; and throw exceptions only under conditions that are truly exceptional.
CH4: Efficiency
Item 16: Remember the 80-20 rule
-
The overall performance of your software is almost always determined by a small part of its constituent code.
-
The best way to guard against these kinds of pathological results is to profile your software using as many data sets as possible.
Item 17: Consider using lazy evaluation
-
When you employ lazy evaluation, you write your classes in such a way that they defer computations until the results of those computations are required.
- To avoid unnecessary copying of objects.
- To distinguish reads from writes using
operator[]
. - To avoid unnecessary reads from databases.
- To avoid unnecessary numerical computations.
-
Lazy evaluation is only useful when there’s a reasonable chance your software will be asked to perform computations that can be avoided.
1// Example 1: Reference Counting 2class String; 3String s1 = "Hello"; 4String s2 = s1; // call String copy constructor 5std::cout << s1; // read s1's value 6std::cout << s1 + s2; // read s1's and s2's values 7s2.convertToUpperCase(); // don't bother to make a copy of something until you really need one 8 9// Example 2: Distinguish Reads from Writes 10String s = "Homer's Iliad"; 11std::cout << s[3]; // call operator[] to read s[3] 12s[3] = 'x'; // call operator[] to write s[3] 13 14// Example 3: Lazy Fetching 15class ObjectID; 16class LargeObject { 17 public: 18 LargeObject(ObjectID id); 19 const std::string& field1() const; 20 int field2() const; 21 double field3() const; 22 const std::string& field4() const; 23 // ... 24 private: 25 ObjectID oid; 26 mutable std::string* field1Value; 27 mutable int* field2Value; 28 mutable double* field3Value; 29 mutable std::string* field4Value; 30}; 31 32LargeObject::LargeObject(ObjectID id) 33 : oid(id), field1Value(0), field2Value(0), field3Value(0), field4Value(0) {} 34 35const std::string& LargeObject::field1() const { 36 if (field1Value == 0) { 37 // Read the data for field 1 from the database and make field1Value point to it 38 } 39 return *field1Value; 40} 41 42// Example 4: Lazy Expression Evaluation 43template <class T> 44class Matrix {}; 45 46Matrix<int> m1(1000, 1000); 47Matrix<int> m2(1000, 1000); 48 49Matrix<int> m3 = m1 + m2; // set up a data structure inside m3 that includes some information 50 51Matrix<int> m4(1000, 1000); 52m3 = m4 * m1; // no need to actually calculate the result of m1 + m2 previously
Item 18: Amortize the cost of expected computations
-
Over-eager evaluation is a technique for improving the efficiency of programs when you must support operations whose results are almost always needed or whose results are often needed more than once.
- Caching values that have already been computed and are likely to be needed again.
- Prefetching demands a place to put the things that are prefetched, but it reduces the time need to access those things.
Item 19: Understand the origin of temporary objects
-
True temporary objects in C++ are invisible – they don’t appear in your source code. They arise whenever a non-heap object is created but no named.
-
When implicit type conversions are applied to make function calls succeed.
-
When functions return objects.
1// Situation 1: 2unsigned int countChar(const std::string& str, char ch); 3char buffer[MAX_STRING_LEN]; 4char c; 5 6// Create a temporary object of type string and the object is initialized by calling 7// the string constructor with buffer as its argument 8// These conversions occur only when passing objects by value or when passing to a 9// reference-to-const parameter 10countChar(buffer, c); 11 12// Situation 2: 13class Number; 14// The return value of this function is a temporary 15const Number operator+(const Number& lhs, const Number& rhs);
-
Item 20: Facilitate the return value optimization
-
It is frequently possible to write functions that return objects in such a way that compilers can eliminate the cost of the temporaries. The trick is to return constructor arguments instead of objects.
1class Rational { 2 public: 3 Rational(int numerator = 0, int denominator = 1); 4 // ... 5 int numerator() const; 6 int denominator() const; 7 8 private: 9 int numerator; 10 int denominator; 11}; 12 13// Usage 1: without RVO 14// Step 1: call constructor to initialize result 15// Step 2: call copy constructor to copy local variable result from callee to caller (temporary 16// variable) 17// Step 3: call destructor to destruct local variable result 18const Rational operator*(const Rational& lhs, const Rational& rhs) { 19 Rational result(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); 20 21 return result; 22} 23 24// Usage 2: without RVO 25// Step: caller directly initialize variable defined by the return expression inside the allocated 26// memory 27const Rational operator*(const Rational& lhs, const Rational& rhs) { 28 return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); 29}
Item 21: Overload to avoid implicit type conversions
-
By declaring several functions, each with a different set of parameter types to eliminate the need for type conversions.
1class UPInt { 2 public: 3 UPInt(); 4 UPInt(int value); 5 // ... 6}; 7 8// Overloaded functions to eliminate type conversions 9const UPInt operator+(const UPInt& lhs, const UPInt& rhs); 10const UPInt operator+(const UPInt& lhs, int rhs); 11const UPInt operator+(int lhs, const UPInt& rhs); 12 13// Not allowed 14// Every overloaded operator must take at least one argument of a user-defined type 15const UPInt operator+(int lhs, int rhs);
Item 22: Consider using op=
instead of strand-alone op
-
A good way to ensure that the natural relationship between the assignment version of an operator (e.g.,
operator+=
) and the stand-alone version (e.g.,operator+
) exists is to implement the latter in terms of the former. -
In general, assignment versions of operators are more efficient than stand-alone versions, because stand-alone versions must typically return a new object, and that costs us the construction and destruction of a temporary. Assignment versions of operators write to their left-hand argument, so there is no need to generate a temporary to hold the operator’s return value.
-
By offering assignment versions of operators as well as stand-alone versions, you allow clients of your classes to make the difficult trade-off between efficiency and convenience.
1class Rational { 2 public: 3 // ... 4 Rational& operator+=(const Rational& rhs); 5 Rational& operator-=(const Rational& rhs); 6}; 7 8const Rational operator+(const Rational& lhs, const Rational& rhs) { return Rational(lhs) += rhs; } 9const Rational operator-(const Rational& lhs, const Rational& rhs) { return Rational(lhs) += rhs; } 10 11// Eliminate the need to write the stand-alone functions 12// The corresponding stand-alone operator will automatically be generated if it's needed 13template <class T> 14const T operator+(const T& lhs, const T& rhs) { 15 return T(lhs) += rhs; 16} 17template <class T> 18const T operator-(const T& lhs, const T& rhs) { 19 return T(lhs) -= rhs; 20}
Item 23: Consider alternative libraries
- Different libraries embody different design decisions regarding efficiency, extensibility, portability, type safety, and other issues. You can sometimes significantly improve the efficiency of your software by switching to libraries whose designers gave more weight to performance considerations than to other factors.
Item 24: Understand the costs of virtual functions, multiple inheritance, virtual base classes, and RTTI
Feature | Increases Size of Objects |
Increases Per-Class Data |
Reduces Inlining |
---|---|---|---|
Virtual Functions | Yes | Yes | Yes |
Multiple Inheritance | Yes | Yes | No |
Virtual Base Classes | Yes | No | No |
RTTI | No | Yes | No |
CH5: Techniques
Item 25: Virtualizing constructors and non-member functions
-
A virtual constructor is a function that creates different types of objects depending on the input it is given.
-
A virtual copy constructor returns a pointer to a new copy of the object invoking the function.
-
No longer must a derived class’s redefinition of a base class’s virtual function declare the same return type.
-
You write virtual functions to do the work, then write a non-virtual function that does nothing but call the virtual function.
1class NLComponent { 2 public: 3 virtual NLComponent* clone() const = 0; // virtual copy constructor 4 virtual std::ostream& print(std::ostream& s) const = 0; 5 // ... 6}; 7 8class TextBlock : public NLComponent { 9 public: 10 virtual TextBlock* clone() const { return new TextBlock(*this); } // virtual copy constructor 11 virtual std::ostream& print(std::ostream& s) const; 12 // ... 13}; 14 15class Graphic : public NLComponent { 16 public: 17 virtual Graphic* clone() const { return new Graphic(*this); } // virtual copy constructor 18 virtual std::ostream& print(std::ostream& s) const; 19 // ... 20}; 21 22class NewsLetter { 23 public: 24 NewsLetter(std::istream& str); 25 26 private: 27 static std::list<NLComponent*> components; 28}; 29 30NLComponent* readComponent(std::istream& str); // virtual constructor 31 32NewsLetter::NewsLetter(std::istream& str) { 33 while (str) { 34 components.push_back(readComponent(str)); 35 } 36} 37 38// Make non-member functions act virtual 39inline std::ostream& operator<<(std::ostream& s, const NLComponent& c) { return c.print(s); }
Item 26: Limiting the number of objects of a class
-
The easiest way to prevent objects of a particular class from being created is to declare the constructors of that class private.
- If you create an inline non-member function containing a local static object, you may end up with more than one copy of the static object in your program due to internal linkage.
-
The object construction can exist in three different contexts: on their own, as base class parts of more derived objects, and embedded inside larger objects.
1// Allowing zero or one objects 2class PrintJob { 3 public: 4 PrintJob(const std::string& whatToPrint); 5 // ... 6}; 7 8class Printer { 9 public: 10 void submitJob(const PrintJob& job); 11 void reset(); 12 void performSelfTest(); 13 // ... 14 15 // Design choice 1: be friend 16 friend Printer& thePrinter(); 17 // Design choice 2: be static 18 static Printer& thePrinter(); 19 20 private: 21 Printer(); 22 Printer(const Printer& rhs); 23 // ... 24}; 25 26// It's important that the single Printer object be static in a function and not in a class. 27// An object that's static in a class is, for all intents and purposes, always constructed (and 28// destructed), even if it's never used. In contrast, an object that's static in a function is 29// created the first time through the function, so if the function is never called, the object is 30// never created 31 32// Design choice 1: be friend 33Printer& thePrinter() { 34 static Printer p; 35 return p; 36} 37thePrinter().reset(); 38thePrinter().submitJob(std::string()); 39 40// Design choice 2: be static 41Printer& Printer::thePrinter() { 42 static Printer p; 43 return p; 44} 45Printer::thePrinter().reset(); 46Printer::thePrinter().submitJob(std::string());
1// Allowing objects to come and go 2class Printer { 3 public: 4 class TooManyObjects {}; 5 static Printer* makePrinter(); 6 static Printer* makePrinter(const Printer& rhs); 7 // ... 8 9 private: 10 static unsigned int numObjects; 11 static const int maxObjects = 10; 12 13 Printer(); 14 Printer(const Printer& rhs); 15}; 16 17unsigned int Printer::numObjects = 0; 18const int Printer::maxObjects; 19 20Printer::Printer() { 21 if (numObjects >= maxObjects) { 22 throw TooManyObjects(); 23 } 24 // ... 25} 26 27Printer::Printer(const Printer& rhs) { 28 if (numObjects >= maxObjects) { 29 throw TooManyObjects(); 30 } 31 // ... 32} 33 34Printer* Printer::makePrinter() { return new Printer; } 35 36Printer* Printer::makePrinter(const Printer& rhs) { return new Printer(rhs); }
1// An object-counting base class 2template <class BeingCounted> 3class Counted { 4 public: 5 class TooManyObjects {}; 6 static int objectCount() { return numObjects; } 7 8 protected: 9 Counted(); 10 Counted(const Counted& rhs); 11 ~Counted() { --numObjects; } 12 13 private: 14 static int numObjects; 15 static int maxObjects; 16 void init(); 17}; 18 19template <class BeingCounted> 20Counted<BeingCounted>::Counted() { 21 init(); 22} 23 24template <class BeingCounted> 25Counted<BeingCounted>::Counted(const Counted<BeingCounted>&) { 26 init(); 27} 28 29template <class BeingCounted> 30void Counted<BeingCounted>::init() { 31 if (numObjects >= maxObjects) { 32 throw TooManyObjects(); 33 } 34 ++numObjects; 35} 36 37class PrintJob; 38 39class Printer : private Counted<Printer> { 40 public: 41 static Printer* makePrinter(); 42 static Printer* makePrinter(const Printer& rhs); 43 ~Printer(); 44 void submitJob(const PrintJob& job); 45 void reset(); 46 void performSelfTest(); 47 48 // Make public for clients of Printer 49 using Counted<Printer>::objectCount; 50 using Counted<Printer>::TooManyObjects; 51 52 private: 53 Printer(); 54 Printer(const Printer& rhs); 55};
Item 27: Requiring or prohibiting heap-based objects
-
Restricting access to a class’s destructor or its constructors also prevents both inheritance and containment.
- The inheritance problem can be solved by making a class’s destructor protected.
- The classes that need to contain heap-based objects can be modified to contain pointers to them.
-
There’s not only no portable way to determine whether an object is on the heap, there isn’t even a semi-portable way that works most of the time.
- A mixin (“mix in”) class offers derived classes the ability to determine whether a pointer was allocated from
operator new
.
- A mixin (“mix in”) class offers derived classes the ability to determine whether a pointer was allocated from
-
The fact that the stack-based class’s
operator new
is private has no effect on attempts to allocate objects containing them as members.1// Require heap-based objects 2class UPNumber { 3 public: 4 UPNumber(); 5 UPNumber(int initValue); 6 UPNumber(double initValue); 7 UPNumber(const UPNumber& rhs); 8 // pseudo-destructor 9 void destroy() const { delete this; } 10 // ... 11 12 protected: 13 ~UPNumber(); 14}; 15 16// Derived classes have access to protected members 17class NonNegativeNumber : public UPNumber {}; 18 19// Contain pointers to head-based objects 20class Asset { 21 public: 22 Asset(int initValue); 23 ~Asset(); 24 // ... 25 26 private: 27 UPNumber* value; 28}; 29 30Asset::Asset(int initValue) : value(new UPNumber(initValue)) {} 31Asset::~Asset() { value->destroy(); }
1// Determine whether an object is on the heap 2class HeapTracked { 3 public: 4 class MissingAddress {}; 5 virtual ~HeapTracked() = 0; 6 static void *operator new(size_t size); 7 static void operator delete(void *ptr); 8 bool isOnHeap() const; 9 10 private: 11 typedef const void *RawAddress; 12 static std::list<RawAddress> addresses; 13}; 14 15std::list<HeapTracked::RawAddress> HeapTracked::addresses; 16 17HeapTracked::~HeapTracked() {} 18 19void *HeapTracked::operator new(size_t size) { 20 void *memPtr = ::operator new(size); 21 addresses.push_back(memPtr); 22 return memPtr; 23} 24 25void HeapTracked::operator delete(void *ptr) { 26 std::list<RawAddress>::iterator it = std::find(addresses.begin(), addresses.end(), ptr); 27 if (it != addresses.end()) { 28 addresses.erase(it); 29 ::operator delete(ptr); 30 } else { 31 throw MissingAddress(); 32 } 33} 34 35// dynamic_cast is applicable only to pointers to objects that have at least one virtual function 36// and it gives us a pointer to the beginning of the memory for the current object 37bool HeapTracked::isOnHeap() const { 38 const void *rawAddress = dynamic_cast<const void *>(this); 39 std::list<RawAddress>::iterator it = std::find(addresses.begin(), addresses.end(), rawAddress); 40 return it != addresses.end(); 41} 42 43class Asset : public HeapTracked { 44 private: 45 UPNumber value; 46 // ... 47}; 48 49void inventoryAsset(const Asset *ap) { 50 if (ap->isOnHeap()) { 51 // ... 52 } else { 53 // ... 54 } 55}
1// Prohibit heap-based objects 2class Asset { 3 public: 4 Asset(int initValue); 5 // ... 6 7 private: 8 UPNumber value; // has private operator new 9}; 10 11Asset* pa = new Asset(100); // calls Asset::operator new or ::operator new, 12 // not UPNumber::operator new
Item 28: Smart pointers
-
Passing
auto_ptr
s by value, then, is something to be done only if you’re sure you want to transfer ownership of an object to a (transient) function parameter. -
The
operator*
function just returns a reference to the pointed-to object such thatpointee
need not point to an object of typeT
, while it may point to an object of a class derived fromT
. Theoperator->
function returns a dumb pointer to an object or another smart pointer object as it must be legal to apply the member-selection operator (->) to it. -
Overload
operator!
for your smart pointer classes so thatoperator!
returnstrue
if and only if the smart pointer on which it’s invoked is null. -
Do not provide implicit conversion operators to dumb pointers unless there is a compelling reason to do so.
-
Use member function templates to generate smart pointer conversion functions for inheritance-based type conversions.
- Smart pointers employ member functions as conversion operators, and as far as C++ compilers are concerned, all calls to conversion functions are equally good. The best we can do is to use member templates to generate conversion functions, then use casts in those cases where ambiguity results.
-
Implement smart pointers by having each smart pointer-to-
T
-class publicly inherit from a corresponding smart pointer-to-const
-T
class.1// Test smart pointers for nullness 2template <class T> 3class SmartPtr { 4 public: 5 // Could work, but ... 6 operator void*(); 7}; 8 9class TreeNode; 10class Apple; 11class Orange; 12 13SmartPtr<TreeNode> ptn; 14 15if (ptn == 0) // now fine 16if (ptn) // also fine 17if (!ptn) // fine 18 19SmartPtr<Apple> pa; 20SmartPtr<Orange> po; 21 22if (pa == po); // this compiles! 23 24template <class T> 25class SmartPtr { 26 public: 27 // Much better 28 bool operator!() const; 29}; 30 31if (!ptn) { 32 // ... 33} else { 34 // ... 35}
1// Smart pointers and const 2template <class T> 3class SmartPtrToConst { 4 // ... 5 protected: 6 union { 7 const T* constPointee; 8 T* pointee; 9 }; 10}; 11 12template <class T> 13class SmartPtr : public SmartPtrToConst<T> { 14 // ... 15}; 16 17class CD; 18SmartPtr<CD> pCD = new CD("Famous Movie Themes"); 19SmartPtrToConst<CD> pConstCD = pCD; // fine
Item 29: Reference counting
-
Reference counting is most useful for improving efficiency under the following conditions.
- Relatively few values are shared by relatively many objects.
- Object values are expensive to create or destroy, or they use lots of memory.
1// base class for reference-counted objects 2class RCObject { 3public: 4 void addReference(); 5 void removeReference(); 6 void markUnshareable(); 7 bool isShareable() const; 8 bool isShared() const; 9 10protected: 11 RCObject(); 12 RCObject(const RCObject& rhs); 13 RCObject& operator=(const RCObject& rhs); 14 virtual ~RCObject(); 15 16private: 17 int refCount; 18 bool shareable; 19}; 20 21RCObject::RCObject() : refCount(0), shareable(true) {} 22 23RCObject::RCObject(const RCObject&) : refCount(0), shareable(true) {} 24 25RCObject& RCObject::operator=(const RCObject&) { return *this; } 26 27RCObject::~RCObject() {} 28 29void RCObject::addReference() { ++refCount; } 30 31void RCObject::removeReference() { 32 if (--refCount == 0) { 33 delete this; 34 } 35} 36 37void RCObject::markUnshareable() { shareable = false; } 38 39bool RCObject::isShareable() const { return shareable; } 40 41bool RCObject::isShared() const { return refCount > 1; }
1// template class for smart pointers-to-T objects 2// T must inherit from RCObject 3template <class T> 4class RCPtr { 5public: 6 RCPtr(T* realPtr = 0); 7 RCPtr(const RCPtr& rhs); 8 ~RCPtr(); 9 RCPtr& operator=(const RCPtr& rhs); 10 T* operator->() const; 11 T& operator*() const; 12 13private: 14 T* pointee; 15 void init(); 16}; 17 18template <class T> 19void RCPtr<T>::init() { 20 if (pointee == 0) { 21 return; 22 } 23 if (pointee->isShareable() == false) { 24 pointee = new T(*pointee); 25 } 26 pointee->addReference(); 27} 28 29template <class T> 30RCPtr<T>::RCPtr(T* realPtr) : pointee(realPtr) { 31 init(); 32} 33 34template <class T> 35RCPtr<T>::RCPtr(const RCPtr& rhs) : pointee(rhs.pointee) { 36 init(); 37} 38 39template <class T> 40RCPtr<T>::~RCPtr() { 41 if (pointee) { 42 pointee->removeReference(); 43 } 44} 45 46template <class T> 47RCPtr<T>& RCPtr<T>::operator=(const RCPtr& rhs) { 48 if (pointee != rhs.pointee) { 49 if (pointee) { 50 pointee->removeReference(); 51 } 52 pointee = rhs.pointee; 53 init(); 54 } 55 return *this; 56} 57 58template <class T> 59T* RCPtr<T>::operator->() const { 60 return pointee; 61} 62 63template <class T> 64T& RCPtr<T>::operator*() const { 65 return *pointee; 66}
1// class to be used by application developers 2class String { 3public: 4 String(const char* value = ""); 5 char operator[](int index) const; 6 char& operator[](int index); 7 8private: 9 struct StringValue : public RCObject { 10 char* data; 11 StringValue(const char* initValue); 12 StringValue(const StringValue& rhs); 13 void init(const char* initValue); 14 ~StringValue(); 15 friend class RCPtr<StringValue>; 16 }; 17 RCPtr<StringValue> value; 18}; 19 20void String::StringValue::init(const char* initValue) { 21 data = new char[strlen(initValue) + 1]; 22 strcpy(data, initValue); 23} 24 25String::StringValue::StringValue(const char* initValue) { init(initValue); } 26 27String::StringValue::StringValue(const StringValue& rhs) { init(rhs.data); } 28 29String::StringValue::~StringValue() { delete[] data; } 30 31String::String(const char* initValue) : value(new StringValue(initValue)) {} 32 33char String::operator[](int index) const { return value->data[index]; } 34 35char& String::operator[](int index) { 36 if (value->isShared()) { 37 value = new StringValue(value->data); 38 } 39 value->markUnshareable(); 40 return value->data[index]; 41}
Item 30: Proxy classes
-
Objects that stand for other objects are often called proxy objects, and the classes that give rise to proxy objects are often called proxy classes.
-
Taking the address of a proxy class yields a different type of pointer than does taking the address of a real object.
1class String { 2 public: 3 class CharProxy { 4 public: 5 CharProxy(String& str, int index); // creation 6 CharProxy& operator=(const CharProxy& rhs); // lvalues uses 7 CharProxy& operator=(char c); // lvalues uses 8 operator char() const; // rvalue uses 9 10 private: 11 String& theString; 12 int charIndex; 13 }; 14 15 const CharProxy operator[](int index) const; // for const Strings 16 CharProxy operator[](int index); // for non-const Strings 17 // ... 18 19 friend class CharProxy; 20 21 private: 22 RCPtr<StringValue> value; 23}; 24 25const String::CharProxy String::operator[](int index) const { 26 return CharProxy(const_cast<String&>(*this), index); 27} 28 29String::CharProxy String::operator[](int index) { return CharProxy(*this, index); } 30 31String::CharProxy::CharProxy(String& str, int index) : theString(str), charIndex(index) {} 32 33// Because this function returns a character by value, and because C++ limits the use of such 34// by-value returns to rvalue contexts only, this conversion function can be used only in places 35// where an rvalue is legal. 36String::CharProxy::operator char() const { return theString.value->data[charIndex]; } 37 38// Move the code implementing a write into CharProxy's assignment operators, and that allows us to 39// avoid paying for a write when the non-const operator[] is used only in an rvalue context. 40String::CharProxy& String::CharProxy::operator=(const CharProxy& rhs) { 41 if (theString.value->isShared()) { 42 theString.value = new StringValue(theString.value->data); 43 } 44 theString.value->data[charIndex] = rhs.theString.value->data[rhs.charIndex]; 45 return *this; 46} 47 48String::CharProxy& String::CharProxy::operator=(char c) { 49 if (theString.value->isShared()) { 50 theString.value = new StringValue(theString.value->data); 51 } 52 theString.value->data[charIndex] = c; 53 return *this; 54}
Item 31: Making functions virtual with respect to more than one object
-
The most common approach to double-dispatching is via chains of
if-then-else
s. -
To minimize the risks inherent in an RTTI approach, the strategy is to implement double-dispatching as two single dispatches.
-
Use a vtbl to eliminate the need for compilers to perform chains of
if-then-else
-like computations, and it allows compilers to generate the same code at all virtual function call sites. -
The recompilation problem would go away if our associative array contained pointers to non-member functions.
- Everything in an unnamed namespace is private to the current translation unit (essentially the current file) – it’s just like the functions were declared
static
at file scope.
1// Use virtual functions and RTTI 2class GameObject { 3 public: 4 virtual void collide(GameObject& otherObject) = 0; 5 // ... 6}; 7 8class SpaceShip : public GameObject { 9 public: 10 virtual void collide(GameObject& otherObject); 11 // ... 12}; 13 14class SpaceStation : public GameObject { 15 public: 16 virtual void collide(GameObject& otherObject); 17 // ... 18}; 19 20class Asteroid : public GameObject { 21 public: 22 virtual void collide(GameObject& otherObject); 23 // ... 24}; 25 26class CollisionWithUnknownObject { 27 public: 28 CollisionWithUnknownObject(GameObject& whatWeHit); 29 // ... 30}; 31 32void SpaceShip::collide(GameObject& otherObject) { 33 const type_info& objectType = typeid(otherObject); 34 if (objectType == typeid(SpaceShip)) { 35 SpaceShip& ss = static_cast<SpaceShip&>(otherObject); 36 // process a SpaceShip-SpaceShip collision 37 } else if (objectType == typeid(SpaceStation)) { 38 SpaceStation& ss = static_cast<SpaceStation&>(otherObject); 39 // process a SpaceShip-SpaceStation collision 40 } else if (objectType == typeid(Asteroid)) { 41 Asteroid& ss = static_cast<Asteroid&>(otherObject); 42 // process a SpaceShip-Asteroid collision 43 } else { 44 throw CollisionWithUnknownObject(otherObject); 45 } 46}
1// Use virtual functions only 2class SpaceShip; 3class SpaceStation; 4class Asteroid; 5 6class GameObject { 7 public: 8 virtual void collide(GameObject& otherObject) = 0; 9 virtual void collide(SpaceShip& otherObject) = 0; 10 virtual void collide(SpaceStation& otherObject) = 0; 11 virtual void collide(Asteroid& otherObject) = 0; 12 // ... 13}; 14 15class SpaceShip : public GameObject { 16 public: 17 virtual void collide(GameObject& otherObject); 18 virtual void collide(SpaceShip& otherObject); 19 virtual void collide(SpaceStation& otherObject); 20 virtual void collide(Asteroid& otherObject); 21 // ... 22}; 23 24// Compilers figure out which of a set of functions to call on the basis of the static types of the 25// arguments passed to the function. 26void SpaceShip::collide(GameObject& otherObject) { otherObject.collide(*this); }
1// Emulate virtual function tables 2class GameObject { 3 public: 4 virtual void collide(GameObject& otherObject) = 0; 5 // ... 6}; 7 8class SpaceShip : public GameObject { 9 public: 10 virtual void collide(GameObject& otherObject); 11 virtual void hitSpaceShip(GameObject& otherObject); 12 virtual void hitSpaceStation(GameObject& otherObject); 13 virtual void hitAsteroid(GameObject& otherObject); 14 // ... 15 16 private: 17 typedef void (SpaceShip::*HitFunctionPtr)(GameObject&); 18 typedef std::map<std::string, HitFunctionPtr> HitMap; 19 HitFunctionPtr lookup(const GameObject& whatWeHit) const; 20 static HitMap* initializeCollisionMap(); 21 // ... 22}; 23 24void SpaceShip::hitSpaceShip(GameObject& spaceShip) { 25 SpaceShip& otherShip = dynamic_cast<SpaceShip&>(spaceShip); 26 // ... 27} 28 29void SpaceShip::hitSpaceShip(GameObject& spaceShip) { 30 SpaceShip& otherShip = dynamic_cast<SpaceShip&>(spaceShip); 31 // ... 32} 33 34void SpaceShip::collide(GameObject& otherObject) { 35 HitFunctionPtr hfp = lookup(otherObject); 36 if (hfp) { 37 (this->*hfp)(otherObject); 38 } else { 39 throw CollisionWithUnknownObject(otherObject); 40 } 41} 42 43SpaceShip::HitFunctionPtr SpaceShip::lookup(const GameObject& whatWeHit) const { 44 static std::unique_ptr<HitMap> collisionMap(initializeCollisionMap()); 45 HitMap::iterator mapEntry = collisionMap->find(typeid(whatWeHit).name()); 46 if (mapEntry == collisionMap->end()) { 47 return 0; 48 } 49 return (*mapEntry).second; 50} 51 52SpaceShip::HitMap* SpaceShip::initializeCollisionMap() { 53 HitMap* phm = new HitMap; 54 (*phm)["SpaceShip"] = &hitSpaceShip; 55 (*phm)["SpaceStation"] = &hitSpaceStation; 56 (*phm)["Asteroid"] = &hitAsteroid; 57 return phm; 58}
- Everything in an unnamed namespace is private to the current translation unit (essentially the current file) – it’s just like the functions were declared
CH6: Miscellany
Item 32: Program in the future tense
-
Provide complete classes, even if some parts aren’t currently used. When new demands are made on your classes, you’re less likely to have to go back and modify them.
-
Design your interfaces to facilitate common operations and prevent common errors. Make the classes easy to use correctly, hard to use incorrectly.
-
If there is no great penalty for generalizing your code, generalize it.
Item 33: Make non-leaf classes abstract
-
Non-leaf classes should be abstract. Adherence to it will yield dividends in the form of increased reliability, robustness, comprehensibility, and extensibility throughout your software.
1// Wrong: partial assignment 2class Animal { 3 public: 4 Animal& operator=(const Animal& rhs); 5 // ... 6}; 7 8class Lizard : public Animal { 9 public: 10 Lizard& operator=(const Lizard& rhs); 11 // ... 12}; 13 14class Chicken : public Animal { 15 public: 16 Chicken& operator=(const Chicken& rhs); 17 // ... 18}; 19 20Lizard liz1; 21Lizard liz2; 22Animal* pAnimal1 = &liz1; 23Animal* pAnimal2 = &liz2; 24// Only the Animal part of liz1 will be modified 25*pAnimal1 = *pAnimal2;
1// The goal is to identify useful abstractions and to force them - and only them - into existence as 2// abstract classes 3class AbstractAnimal { 4 protected: 5 AbstractAnimal& operator=(const AbstractAnimal& rhs); 6 7 public: 8 virtual ~AbstractAnimal() = 0; 9 // ... 10}; 11 12class Animal : public AbstractAnimal { 13 public: 14 Animal& operator=(const Animal& rhs); 15 // ... 16}; 17 18class Lizard : public AbstractAnimal { 19 public: 20 Lizard& operator=(const Lizard& rhs); 21 // ... 22}; 23 24class Chicken : public AbstractAnimal { 25 public: 26 Chicken& operator=(const Chicken& rhs); 27 // ... 28};
Item 34: Understand how to combine C++ and C in the same program
-
Make sure the C++ and C compilers produce compatible object files.
-
Declare functions to be used by both languages
extern "C"
. -
If at all possible, write
main
in C++. -
Always use
delete
with memory fromnew
; always usefree
with memory frommalloc
. -
Limit what you pass between the two languages to data structures that compile under C; the C++ version of structs may contain non-virtual member functions.
1// Name mangling 2#ifdef __cplusplus 3extern "C" { 4#endif 5void drawLine(int x1, int y1, int x2, int y2); 6void twiddleBits(unsigned char bits); 7void simulate(int iterations); 8// ... 9#ifdef __cplusplus 10} 11#endif
Item 35: Familiarize yourself with the language standard
-
New features have been added.
-
Templates have been extended.
-
Exception handling has been refined.
-
Memory allocation routines have been modified.
-
New casting forms have been added.
-
Language rules have been refined.
-
Support for the standard C library.
-
Support for strings.
-
Support for localization.
-
Support for I/O.
-
Support for numeric applications.
-
Support for general-purpose containers and algorithms.