[NOTE] More Effective C++

33 minute read

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 the constness or volatileness 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 constness.
  • 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;
    

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&& and operator||, 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 or expression2 well be evaluated first.
  • 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 the new 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 is operator new.
    • Second, it calls a constructor to initialize an object in the memory that was allocated.
    • A special version of operator new called placement new allows you to call a constructor directly.
  • The function operator delete is to the built-in delete operator as operator new is to the new 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 allow const 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}
    

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.
  • 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 a catch 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}
    

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, and bad_exception are all objects, not pointers to objects.
  • 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 call terminate, and the default behavior for terminate is to call abort, 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.
  • 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_ptrs 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 that pointee need not point to an object of type T, while it may point to an object of a class derived from T. The operator-> 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 that operator! returns true 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-elses.

  • 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}
    

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 from new; always use free with memory from malloc.

  • 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.