Item 4: Avoid gratuitous default constructors.
A default constructor (i.e., a constructor that can be called with no arguments) is the C++ way of saying you can get something for nothing. Constructors initialize objects, so default constructors initialize objects without any information from the place where the object is being created. Sometimes this makes perfect sense. Objects that act like numbers, for example, may reasonably be initialized to zero or to undefined values. Objects that act like pointers (
Item 28) may reasonably be initialized to null or to undefined values. Data structures like linked lists, hash tables, maps, and the like may reasonably be initialized to empty
Not all objects fall into this category. For many objects, there is no reasonable way to perform a complete initialization in the absence of outside information. For example, an object representing an entry in an address book makes no sense unless the name of the thing being entered is provided. In some companies, all equipment must be tagged with a corporate ID number, and creating an object to model a piece of equipment in such companies is nonsensical unless the appropriate ID number is
In a perfect world, classes in which objects could reasonably be created from nothing would contain default constructors and classes in which information was required for object construction would not. Alas, ours is not the best of all possible worlds, so we must take additional concerns into account. In particular, if a class lacks a default constructor, there are restrictions on how you can use that
Consider a class for company equipment in which the corporate ID number of the equipment is a mandatory constructor
class EquipmentPiece { public: EquipmentPiece(int IDNumber); ... };
Because EquipmentPiece
lacks a default constructor, its use may be problematic in three contexts. The first is the creation of arrays. There is, in general, no way to specify constructor arguments for objects in arrays, so it is not usually possible to create arrays of EquipmentPiece
EquipmentPiece bestPieces[10]; // error! No way to call // EquipmentPiece ctors EquipmentPiece *bestPieces = new EquipmentPiece[10]; // error! same problem
There are three ways to get around this restriction. A solution for non-heap arrays is to provide the necessary arguments at the point where the array is
int ID1, ID2, ID3, ..., ID10; // variables to hold // equipment ID numbers ... EquipmentPiece bestPieces[] = { // fine, ctor arguments EquipmentPiece(ID1), // are provided EquipmentPiece(ID2), EquipmentPiece(ID3), ..., EquipmentPiece(ID10) };
Unfortunately, there is no way to extend this strategy to heap
A more general approach is to use an array of pointers instead of an array of
typedef EquipmentPiece* PEP; // a PEP is a pointer to // an EquipmentPiece PEP bestPieces[10]; // fine, no ctors called
PEP *bestPieces = new PEP[10]; // also fine
Each pointer in the array can then be made to point to a different EquipmentPiece
for (int i = 0; i < 10; ++i) bestPieces[i] = new EquipmentPiece( ID Number );
There are two disadvantages to this approach. First, you have to remember to delete all the objects pointed to by the array. If you forget, you have a resource leak. Second, the total amount of memory you need increases, because you need the space for the pointers as well as the space for the EquipmentPiece
You can avoid the space penalty if you allocate the raw memory for the array, then use "placement new
" (see Item 8) to construct the EquipmentPiece
objects in the
// allocate enough raw memory for an array of 10 // EquipmentPiece objects; see Item 8 for details on // the operatornew[] function void *rawMemory = operatornew[] (10*sizeof(EquipmentPiece));
// make bestPieces point to it so it can be treated as an // EquipmentPiece array EquipmentPiece *bestPieces = static_cast<EquipmentPiece*>(rawMemory);
// construct the EquipmentPiece objects in the memory // using "placement new" (see Item 8) for (int i = 0; i < 10; ++i) new (&bestPieces[i]) EquipmentPiece( ID Number );
Notice that you still have to provide a constructor argument for each EquipmentPiece
object. This technique (as well as the array-of-pointers idea) allows you to create arrays of objects when a class lacks a default constructor; it doesn't show you how to bypass required constructor arguments. There is no way to do that. If there were, it would defeat the purpose of constructors, which is to guarantee that objects are
The downside to using placement new
, aside from the fact that most programmers are unfamiliar with it (which will make maintenance more difficult), is that you must manually call destructors on the objects in the array when you want them to go out of existence, then you must manually deallocate the raw memory by calling operator
(again, see Item 8):
// destruct the objects in bestPieces in the inverse // order in which they were constructed for (int i = 9; i >= 0; --i) bestPieces[i].~EquipmentPiece(); // deallocate the raw memory operatordelete[] (rawMemory);
If you forget this requirement and use the normal array-deletion syntax, your program will behave unpredictably. That's because the result of deleting a pointer that didn't come from the new
operator is undefined:
delete [] bestPieces; // undefined! bestPieces // didn't come from the new // operator
For more information on the new
operator, placement new
and how they interact with constructors and destructors, see Item 8.
The second problem with classes lacking default constructors is that they are ineligible for use with many template-based container classes. That's because it's a common requirement for such templates that the type used to instantiate the template provide a default constructor. This requirement almost always grows out of the fact that inside the template, an array of the template parameter type is being created. For example, a template for an Array
class might look something like this:
template<class T> class Array { public: Array(int size); ... private: T *data; }; template<class T> Array<T>::Array(int size) { data = new T[size]; // calls T::T() for each ... // element of the array }
In most cases, careful template design can eliminate the need for a default constructor. For example, the standard vector
template (which generates classes that act like extensible arrays) has no requirement that its type parameter have a default constructor. Unfortunately, many templates are designed in a manner that is anything but careful. That being the case, classes without default constructors will be incompatible with many templates. As C++ programmers learn more about template design, this problem should recede in significance. How long it will take for that to happen, however, is anyone's
The final consideration in the to-
Because of the restrictions imposed on classes lacking default constructors, some people believe all classes should have them, even if a default constructor doesn't have enough information to fully initialize objects of that class. For example, adherents to this philosophy might modify EquipmentPiece
as
class EquipmentPiece { public: EquipmentPiece(int IDNumber = UNSPECIFIED); ...
private: static const int UNSPECIFIED; // magic ID number value // meaning no ID was }; // specified
This allows EquipmentPiece
objects to be created like
EquipmentPiece e; // now okay
Such a transformation almost always complicates the other member functions of the class, because there is no longer any guarantee that the fields of an EquipmentPiece
object have been meaningfully initialized. Assuming it makes no sense to have an EquipmentPiece
without an ID field, most member functions must check to see if the ID is present. If it's not, they'll have to figure out how to stumble on anyway. Often it's not clear how to do that, and many implementations choose a solution that offers nothing but expediency: they throw an exception or they call a function that terminates the program. When that happens, it's difficult to argue that the overall quality of the software has been improved by including a default constructor in a class where none was
Inclusion of meaningless default constructors affects the efficiency of classes, too. If member functions have to test to see if fields have truly been initialized, clients of those functions have to pay for the time those tests take. Furthermore, they have to pay for the code that goes into those tests, because that makes executables and libraries bigger. They also have to pay for the code that handles the cases where the tests fail. All those costs are avoided if a class's constructors ensure that all fields of an object are correctly initialized. Often default constructors can't offer that kind of assurance, so it's best to avoid them in classes where they make no sense. That places some limits on how such classes can be used, yes, but it also guarantees that when you do use such classes, you can expect that the objects they generate are fully initialized and are efficiently