More Books
Effective C++ 55 Specific Ways to Improve Your Programs and Designs
Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs
Table of Contents
Copyright
Praise for Effective C++, Third Edition
Addison-Wesley Professional Computing Series
Preface
Acknowledgments
Introduction
Terminology
Chapter 1. Accustoming Yourself to C++
Item 1: View C++ as a federation of languages
Item 2: Prefer consts, enums, and inlines to #defines
Item 3: Use const whenever possible
Item 4: Make sure that objects are initialized before they're used
Chapter 2. Constructors, Destructors, and Assignment Operators
Item 5: Know what functions C++ silently writes and calls
Item 6: Explicitly disallow the use of compiler-generated functions you do not want
Item 7: Declare destructors virtual in polymorphic base classes
Item 8: Prevent exceptions from leaving destructors
Item 9: Never call virtual functions during construction or destruction
Item 10: Have assignment operators return a reference to *this
Item 11: Handle assignment to self in operator=
Item 12: Copy all parts of an object
Chapter 3. Resource Management
Item 13: Use objects to manage resources.
Item 14: Think carefully about copying behavior in resource-managing classes.
Item 15: Provide access to raw resources in resource-managing classes.
Item 16: Use the same form in corresponding uses of new and delete.
Item 17: Store newed objects in smart pointers in standalone statements.
Chapter 4. Designs and Declarations
Item 18: Make interfaces easy to use correctly and hard to use incorrectly
Item 19: Treat class design as type design
Item 20: Prefer pass-by-reference-to-const to pass-by-value
Item 21: Don't try to return a reference when you must return an object
Item 22: Declare data members private
Item 23: Prefer non-member non-friend functions to member functions
Item 24: Declare non-member functions when type conversions should apply to all parameters
Item 25: Consider support for a non-throwing swap
Chapter 5. Implementations
Item 26: Postpone variable definitions as long as possible.
Item 27: Minimize casting.
Item 28: Avoid returning "handles" to object internals.
Item29: Strive for exception-safe code.
Item 30: Understand the ins and outs of inlining.
Item31: Minimize compilation dependencies between files.
Chapter 6. Inheritance and Object-Oriented Design
Item 32: Make sure public inheritance models "is-a."
Item 33: Avoid hiding inherited names
Item 34: Differentiate between inheritance of interface and inheritance of implementation
Item 35: Consider alternatives to virtual functions
Item 36: Never redefine an inherited non-virtual function
Item 37: Never redefine a function's inherited default parameter value
Item 38: Model "has-a" or "is-implemented-in-terms-of" through composition
Item 39: Use private inheritance judiciously
Item 40: Use multiple inheritance judiciously
Chapter 7. Templates and Generic Programming
Item 41: Understand implicit interfaces and compile-time polymorphism
Item 42: Understand the two meanings of typename
Item 43: Know how to access names in templatized base classes
Item 44: Factor parameter-independent code out of templates
Item 45: Use member function templates to accept "all compatible types."
Item 46: Define non-member functions inside templates when type conversions are desired
Item 47: Use traits classes for information about types
Item 48: Be aware of template metaprogramming
Chapter 8. Customizing new and delete
Item 49: Understand the behavior of the new-handler
Item 50: Understand when it makes sense to replace new and delete
Item 51: Adhere to convention when writing new and delete
Item 52: Write placement delete if you write placement new
Chapter 9. Miscellany
Item 53: Pay attention to compiler warnings.
Item 54: Familiarize yourself with the standard library, including TR1
Item.55: Familiarize yourself with Boost.
Appendix A. Beyond Effective C++
Appendix B. Item Mappings Between Second and Third Editions
Index
index_SYMBOL
index_A
index_B
index_C
index_D
index_E
index_F
index_G
index_H
index_I
index_J
index_K
index_L
index_M
index_N
index_O
index_P
index_R
index_S
index_T
index_U
index_V
index_W
index_X
index_Z

Item 51: Adhere to convention when writing new and delete

Item 50 explains when you might want to write your own versions of operator new and operator delete, but it doesn't explain the conventions you must follow when you do it. The rules aren't hard to follow, but some of them are unintuitive, so it's important to know what they are.

We'll begin with operator new. Implementing a conformant operator new requires having the right return value, calling the new-handling function when insufficient memory is available (see Item 49), and being prepared to cope with requests for no memory. You'll also want to avoid inadvertently hiding the "normal" form of new, though that's more a class interface issue than an implementation requirement; it's addressed in Item 52.

The return value part of operator new is easy. If you can supply the requested memory, you return a pointer to it. If you can't, you follow the rule described in Item 49 and throw an exception of type bad_alloc.

It's not quite that simple, however, because operator new actually tries to allocate memory more than once, calling the new-handling function after each failure. The assumption here is that the new-handling function might be able to do something to free up some memory. Only when the pointer to the new-handling function is null does operator new throw an exception.

Curiously, C++ requires that operator new return a legitimate pointer even when zero bytes are requested. (Requiring this odd-sounding behavior simplifies things elsewhere in the language.) That being the case, pseudocode for a non-member operator new looks like this:


void * operator new(std::size_t size) throw(std::bad_alloc)

{                                      // your operator new might

  using namespace std;                 // take additional params



  if (size == 0) {                     // handle 0-byte requests

    size = 1;                          // by treating them as

  }                                    // 1-byte requests



  while (true) {

   attempt to allocate size bytes;

    if (the allocation was successful)

       return (a pointer to the memory);



    // allocation was unsuccessful; find out what the

    // current new-handling function is (see below)

    new_handler globalHandler = set_new_handler(0);

    set_new_handler(globalHandler);



    if (globalHandler) (*globalHandler)();

    else throw std::bad_alloc();

  }

}


The trick of treating requests for zero bytes as if they were really requests for one byte looks slimy, but it's simple, it's legal, it works, and how often do you expect to be asked for zero bytes, anyway?

You may also look askance at the place in the pseudocode where the new-handling function pointer is set to null, then promptly reset to what it was originally. Unfortunately, there is no way to get at the new-handling function pointer directly, so you have to call set_new_handler to find out what it is. Crude, yes, but also effective, at least for single-threaded code. In a multithreaded environment, you'll probably need some kind of lock to safely manipulate the (global) data structures behind the new-handling function.

Item 49 remarks that operator new contains an infinite loop, and the code above shows that loop explicitly; "while (true)" is about as infinite as it gets. The only way out of the loop is for memory to be successfully allocated or for the new-handling function to do one of the things described in Item 49: make more memory available, install a different new-handler, deinstall the new-handler, throw an exception of or derived from bad_alloc, or fail to return. It should now be clear why the new-handler must do one of those things. If it doesn't, the loop inside operator new will never terminate.

Many people don't realize that operator new member functions are inherited by derived classes. That can lead to some interesting complications. In the pseudocode for operator new above, notice that the function tries to allocate size bytes (unless size is zero). That makes perfect sense, because that's the argument that was passed to the function. However, as Item 50 explains, one of the most common reasons for writing a custom memory manager is to optimize allocation for objects of a specific class, not for a class or any of its derived classes. That is, given an operator new for a class X, the behavior of that function is typically tuned for objects of size sizeof(X)—nothing larger and nothing smaller. Because of inheritance, however, it is possible that the operator new in a base class will be called to allocate memory for an object of a derived class:


class Base {

public:

  static void * operator new(std::size_t size) throw(std::bad_alloc);

  ...

};



class Derived: public Base                // Derived doesn't declare

{ ... };                                  // operator new



Derived *p = new Derived;                 // calls Base::operator new!


If Base's class-specific operator new wasn't designed to cope with this — and chances are that it wasn't — the best way for it to handle the situation is to slough off calls requesting the "wrong" amount of memory to the standard operator new, like this:


void * Base::operator new(std::size_t size) throw(std::bad_alloc)

{

  if (size != sizeof(Base))               // if size is "wrong,"

     return ::operator new(size);         // have standard operator

                                          // new handle the request



  ...                                     // otherwise handle

                                          // the request here

}


"Hold on!" I hear you cry, "You forgot to check for the pathological-but-nevertheless-possible case where size is zero!" Actually, I didn't, and please stop using hyphens when you cry out. The test is still there, it's just been incorporated into the test of size against sizeof(Base). C++ works in some mysterious ways, and one of those ways is to decree that all freestanding objects have non-zero size (see Item 39). By definition, sizeof(Base) can never be zero, so if size is zero, the request will be forwarded to ::operator new, and it will become that function's responsibility to treat the request in a reasonable fashion.

If you'd like to control memory allocation for arrays on a per-class basis, you need to implement operator new's array-specific cousin, operator new[]. (This function is usually called "array new," because it's hard to figure out how to pronounce "operator new[]".) If you decide to write operator new[], remember that all you're doing is allocating a chunk of raw memory — you can't do anything to the as-yet-nonexistent objects in the array. In fact, you can't even figure out how many objects will be in the array. First, you don't know how big each object is. After all, a base class's operator new[] might, through inheritance, be called to allocate memory for an array of derived class objects, and derived class objects are usually bigger than base class objects.

Hence, you can't assume inside Base::operator new[] that the size of each object going into the array is sizeof(Base), and that means you can't assume that the number of objects in the array is (bytes requested)/sizeof(Base). Second, the size_t parameter passed to operator new[] may be for more memory than will be filled with objects, because, as Item 16 explains, dynamically allocated arrays may include extra space to store the number of array elements.

So much for the conventions you need to follow when writing operator new. For operator delete, things are simpler. About all you need to remember is that C++ guarantees it's always safe to delete the null pointer, so you need to honor that guarantee. Here's pseudocode for a non-member operator delete:


void operator delete(void *rawMemory) throw()

{

  if (rawMemory == 0) return;            // do nothing if the null

                                         // pointer is being deleted



  deallocate the memory pointed to by rawMemory;

}


The member version of this function is simple, too, except you've got to be sure to check the size of what's being deleted. Assuming your class-specific operator new forwards requests of the "wrong" size to ::operator new, you've got to forward "wrongly sized" deletion requests to ::operator delete:


class Base {                            // same as before, but now

public:                                 // operator delete is declared



  static void * operator new(std::size_t size) throw(std::bad_alloc);

  static void operator delete(void *rawMemory, std::size_t size) throw();

  ...

};

void Base::operator delete(void *rawMemory, std::size_t size) throw()

{

  if (rawMemory == 0) return;           // check for null pointer



  if (size != sizeof(Base)) {           // if size is "wrong,"

     ::operator delete(rawMemory);      // have standard operator

     return;                            // delete handle the request

  }

  deallocate the memory pointed to by rawMemory;

  return;

}


Interestingly, the size_t value C++ passes to operator delete may be incorrect if the object being deleted was derived from a base class lacking a virtual destructor. This is reason enough for making sure your base classes have virtual destructors, but Item 7 describes a second, arguably better reason. For now, simply note that if you omit virtual destructors in base classes, operator delete functions may not work correctly.

Things to Remember

  • operator new should contain an infinite loop trying to allocate memory, should call the new-handler if it can't satisfy a memory request, and should handle requests for zero bytes. Class-specific versions should handle requests for larger blocks than expected.

  • operator delete should do nothing if passed a pointer that is null. Class-specific versions should handle blocks that are larger than expected.