Objects are the fundamental unit of abstraction in object-oriented programming. An object, in the broader sense, is a region of memory storage. Class objects have properties that are determined when the object is created. Conceptually, every class object has four special member functions: default constructor, copy constructor, assignment operator, and destructor. If these members are not explicitly declared by the programmer, the implementation implicitly declares them. This chapter surveys the semantics of the special member functions and their role in class design and implementation. This chapter also examines several techniques and guidelines for effective usage of the special member functions.
A constructor is used to initialize an object. A default constructor is one that can be invoked without any arguments. If there is no user-declared constructor for a class, and if the class does not contain const or reference data members, the implementation implicitly declares a default constructor for it.
An implicitly-declared default constructor is an inline public member of its class; it performs the initialization operations that are needed by the implementation to create an object of this type. Note, however, that these operations do not involve initialization of user-declared data members or allocation of memory from the free store. For example
class C { private: int n; char *p; public: virtual ~C() {} }; void f() { C obj; // 1 implicitly-defined constructor is invoked }
The programmer did not declare a constructor in class C -- an implicit default constructor was declared and defined by the implementation in order to create an instance of class C in the line numbered 1. The synthesized constructor does not initialize the data members n and p, nor does it allocate memory for the latter. These data members have an indeterminate value after obj has been constructed.
This is because the synthesized default constructor performs only the initialization operations that are required by the implementation -- not the programmer -- to construct an object. In this case, C is a polymorphic class. An object of this type contains a pointer to the virtual function table of its class. The virtual pointer is initialized by the implicitly-defined constructor.
Other implementation-required operations that are performed by implicitly-defined constructors are the invocation of a base class constructor and the invocation of the constructor of embedded objects. The implementation does not declare a constructor for a class if the programmer has defined one. For example
class C { private: int n; char *p; public: C() : n(0), p(NULL) {} virtual ~C() {} }; void f2() { C obj; // 1 user-defined constructor is invoked }
Now the data members of the object obj are initialized because the user-defined constructor was invoked to create it. Note, however, that the user-defined constructor only initializes the data members n and p. Obviously, the virtual pointer must have been initialized as well -- otherwise, the program will be ill-formed. But when did the initialization of the virtual pointer take place? The compiler augments the user-defined constructor with additional code, which is inserted into the constructor's body before any user-written code, and performs the necessary initialization of the virtual pointer.
Because the virtual pointer is initialized in the constructor before any user-written code, it is safe to call member functions (both virtual and nonvirtual) of an object from its constructor. It is guaranteed that the invoked virtual is the one that is defined in the current object (or of the base class, if it has not been overridden in the current object). However, virtual member functions of objects that are derived from the one whose constructor is executing are not called. For example
class A { public: virtual void f() {} virtual void g() {} }; class B: public A { public: void f () {} // overriding A::f() B() { f(); // calls B::f() g(); // g() was not overriden in B, therefore calling A::g() } }; class C: public B { public: void f () {} //overriding B::f() };
Please note that if the object's member functions refer to data members of the object, it is the 'programmer's responsibility to initialize these data members first -- most preferably with a member-initialization list (member-initialization lists are discussed next). For example
class C { private: int n; int getn() const { cout<<n<<endl; } public: C(int j) : n(j) { getn(); } //Fine: n initialized before getn() is called };
As you have observed, compilers synthesize a default constructor for every class or struct, unless a constructor was already defined by the user. However, in certain conditions, such a synthesized constructor is redundant:
class Empty {}; //class has no base classes, virtual member functions //or embedded objects struct Person { int age; char name[20]; double salary; }; int main() { Empty e; Person p; p.age = 30; return 0; }
An implementation can instantiate Empty and Person objects without a constructor. In such cases, the explicitly-declared constructor is said to be trivial, which means that the implementation does not need it in order to create an instance of its class. A constructor is considered trivial when all the following hold true:
Its class has no virtual member functions and no virtual base classes.
All the direct base classes of the constructor's class have trivial constructors.
All the member objects in the constructor's class have trivial constructors.
You can see that both Empty and Person fulfill these conditions; therefore, each of them has a trivial constructor. The compiler suppresses the automatic synthesis of a trivial constructor, thereby producing code that is as efficient in terms of size and speed as that which is produced by a C compiler.
It is very common to define a class that has more than one constructor. For instance, a string class can define one constructor that takes const char * as an argument, another that takes an argument of type size_t to indicate the initial capacity of the string, and a default constructor.
class string { private: char * pc; size_t capacity; size_t length; enum { DEFAULT_SIZE = 32}; public: string(const char * s); string(size_t initial_capacity ); string(); //...other member functions and overloaded operators };
Each of the three constructors performs individual operations. Nonetheless, some identical tasks -- such as allocating memory from the free store and initializing it, or assigning the value of capacity to reflect the size of the allocated storage -- are performed in every constructor. Instead of repeating identical pieces of code in each of the constructors, it is better to move the recurring code into a single nonpublic member function. This function is called by every constructor. The results are shorter compilation time and easier future maintenance:
class string { private: char * pc; size_t capacity; size_t length; enum { DEFAULT_SIZE = 32}; // the following function is called by every user-defined constructor void init( size_t cap = DEFAULT_SIZE); public: string(const char * s); string(size_t initial_capacity ); string(); //...other member functions and overloaded operators }; void string::init( size_t cap) { pc = new char[cap]; capacity = cap; } string::string(const char * s) { size_t size = strlen (s); init(size + 1); //make room for null terminating character length = size; strcpy(pc, s); } string::string(size_t initial_capacity ) { init(initial_capacity); length=0; } string::string() { init(); length = 0; }
A class might have no default constructor. For example
class File { private: string path; int mode; public: File(const string& file_path, int open_mode); ~File(); };
Class File has a user-defined constructor that takes two arguments. The existence of a user-defined constructor blocks the synthesis of an implicitly-declared default constructor. Because the programmer did not define a default constructor either, class File does not have a default constructor. A class with no default constructor limits its users to a narrower set of allowed uses. For instance, when an array of objects is instantiated, the default constructor -- and only the default constructor -- of each array member is invoked. Therefore, you cannot instantiate arrays thereof unless you use a complete initialization list:
File folder1[10]; //error, array requires default constructor File folder2[2] = { File("f1", 1)}; //error, f2[1] still requires //a default constructor File folder3[3] = { File("f1", 1), File("f2",2), File("f3",3) }; //OK, //fully initialized array Similar difficulties arise when you attempt to store objects that have no default constructor in STL containers:#include <vector> using namespace std; void f() { vector <File> fv(10); //error, File has no default constructor vector <File> v; //OK v.push_back(File("db.dat", 1)); //OK v.resize(10); //error, File has no default constructor v.resize(10, File("f2",2)); //OK }
Was the lack of a default constructor in class File intentional? Maybe. Perhaps the implementer considered an array of File objects undesirable because each object in the array needs to somehow acquire its path and open mode. However, the lack of a default constructor
imposes restrictions that are too draconian for most classes.
In order to qualify as an element in an STL container, an object must possess a copy constructor, an assignment operator, and a destructor as public members (more on this in Chapter 10, "STL and Generic Programming").
A default constructor is also required for certain STL container operations, as you saw in the preceding example.
Many operating systems store the files in a directory as a linked list of file objects. By omitting a default constructor from File, the implementer severely compromises the capability of its users to implement a file system as a std::list<File>.
For a class such as File, whose constructor must initialize its members with user-supplied values, it might still be possible to define a default constructor. Instead of supplying the necessary path and open mode as constructor arguments, a default constructor can read them from a sequential database file.
Still, a default constructor can be undesirable in some cases. One such case is a singleton object. Because a singleton object must have one and only one instance, it is recommended that you block the creation of built-in arrays and containers of such objects by making the default constructor inaccessible. For example
#include<string> using namespace std; int API_getHandle(); //system API function class Application { private: string name; int handle; Application(); // make default constructor inaccessible public: explicit Application(int handle); ~Application(); }; int main() { Application theApp( API_getHandle() ); //ok Application apps[10]; //error, default constructor is inaccessible }
Class Application does not have a default constructor; therefore, it is impossible to create arrays and containers of Application objects. In this case, the lack of a default constructor is intentional (other implementation details are still required to ensure that a single instance -- and only a single instance -- of Application is created. However, making the default constructor inaccessible is one of these details).
Fundamental types such as char, int, and float have constructors, as do user-defined types. You can initialize a variable by explicitly invoking its default constructor:
int main() { char c = char(); int n = int (); return 0; }
The value that is returned by the explicit invocation of the default constructor of a fundamental type is equivalent to casting 0 to that type. In other words,
char c = char();
is equivalent to
char c = char(0);
Of course, it is possible to initialize a fundamental type with values other than 0:
float f = float (0.333); char c = char ('a');
Normally, you use the shorter notation:
char c = 'a'; float f = 0.333;
However, this language extension enables uniform treatment in templates for fundamental types and user-defined types. Fundamental types that are created on the free store using the operator new can be initialized in a similar manner:
int *pi= new int (10); float *pf = new float (0.333);
A constructor that takes a single argument is, by default, an implicit conversion operator, which converts its argument to an object of its class (see also Chapter 3, "Operator Overloading"). Examine the following concrete example:
class string { private: int size; int capacity; char *buff; public: string(); string(int size); // constructor and implicit conversion operator string(const char *); // constructor and implicit conversion operator ~string(); };
Class string has three constructors: a default constructor, a constructor that takes int, and a constructor that constructs a string from const char *. The second constructor is used to create an empty string object with an initial preallocated buffer at the specified size. However, in the case of class string, the automatic conversion is dubious. Converting an int into a string object doesn't make sense, although this is exactly what this constructor does. Consider the following:
int main() { string s = "hello"; //OK, convert a C-string into a string object int ns = 0; s = 1; // 1 oops, programmer intended to write ns = 1, }
In the expression s= 1;, the programmer simply mistyped the name of the variable ns, typing s instead. Normally, the compiler detects the incompatible types and issues an error message. However, before ruling it out, the compiler first searches for a user-defined conversion that allows this expression; indeed, it finds the constructor that takes int. Consequently, the compiler interprets the expression s= 1; as if the programmer had written
s = string(1);
You might encounter a similar problem when calling a function that takes a string argument. The following example can either be a cryptic coding style or simply a programmer's typographical error. However, due to the implicit conversion constructor of class string, it will pass unnoticed:
int f(string s); int main() { f(1); // without a an explicit constructor, //this call is expanded into: f ( string(1) ); //was that intentional or merely a programmer's typo? }
'In order to avoid such implicit conversions, a constructor that takes one argument needs to be declared explicit:
class string { //... public: explicit string(int size); // block implicit conversion string(const char *); //implicit conversion ~string(); };
An explicit constructor does not behave as an implicit conversion operator, which enables the compiler to catch the typographical error this time:
int main() { string s = "hello"; //OK, convert a C-string into a string object int ns = 0; s = 1; // compile time error ; this time the compiler catches the typo }
Why aren't all constructors automatically declared explicit? Under some conditions, the automatic type conversion is useful and well behaved. A good example of this is the third constructor of string:
string(const char *);
The implicit type conversion of const char * to a string object enables its users to write the following:
string s; s = "Hello";
The compiler implicitly transforms this into
string s; //pseudo C++ code: s = string ("Hello"); //create a temporary and assign it to s
On the other hand, if you declare this constructor explicit, you have to use explicit type conversion:
class string { //... public: explicit string(const char *); }; int main() { string s; s = string("Hello"); //explicit conversion now required return 0; }
Extensive amounts of legacy C++ code rely on the implicit conversion of constructors. The C++ Standardization committee was aware of that. In order to not make existing code break, the implicit conversion was retained. However, a new keyword, explicit, was introduced to the languageto enable the programmer to block the implicit conversion when it is undesirable. As a rule, a constructor that can be invoked with a single argument needs to be declared explicit. When the implicit type conversion is intentional and well behaved, the constructor can be used as an implicit conversion operator.
Sometimes it might be useful to disable programmers from instantiating an object of a certain class, for example, a class that is meant to be used only as a base class for others. A protected constructor blocks creation of class instances, yet it does so without disallowing derived objects' instantiation:
class CommonRoot { protected: CommonRoot(){}//no objects of this class should be instantiated virtual ~CommonRoot (); }; class Derived: public CommonRoot { public: Derived(); }; int main() { Derived d; // OK, constructor of d has access to //any protected member in its base class CommonRoot cr; //compilation error: attempt to //access a protected member of CommonRoot }
The same effect of blocking instantiation of a class can be achieved by declaring pure virtual functions. However, these add runtime and space overhead. When pure virtual functions aren't needed, you can use a protected constructor instead.
A constructor might have a member initialization (mem-initialization for short) list that initializes the data members of the class. For example
class Cellphone //1: mem-init { private: long number; bool on; public: Cellphone (long n, bool ison) : number(n), on(ison) {} };
The constructor of Cellphone can also be written as follows:
Cellphone (long n, bool ison) //2 initialization within constructor's body { number = n; on = ison; }
There is no substantial difference between the two forms in the case of Cellphone's constructor. This is due to the way mem-initialization lists are processed by the compiler. The compiler scans the mem-initialization list and inserts the initialization code into the constructor's body before any user-written code. Thus, the constructor in the first example is expanded by the compiler into the constructor in the second example. Nonetheless, the choice between using a mem-initialization list and initialization inside the constructor's body is significant in the following four cases:
Initialization of const members
Initialization of reference members
Passing arguments to a constructor of a base class or an embedded object
Initialization of member objects
In the first three cases, a mem-initialization list is mandatory; in the fourth case, it is optional. Consider the concrete examples that are discussed in the following paragraphs.'
const data members of a class, including const members of a base or embedded subobject, must be initialized in a mem-initialization list.
class Allocator { private: const int chunk_size; public: Allocator(int size) : chunk_size(size) {} };
A reference data member must be initialized by a mem-initialization list.
class Phone; class Modem { private: Phone & line; public: Modem(Phone & ln) : line(ln) {} };
When a constructor has to pass arguments to the constructor of its base class or to the constructor of an embedded object, a mem-initializer must be used.
class base { private: int num1; char * text; public: base(int n1, char * t) {num1 = n1; text = t; } //no default constructor }; class derived : public base { private: char *buf; public: derived (int n, char * t) : base(n, t) //pass arguments to base constructor { buf = (new char[100]);} };
Consider the following example:
#include<string> using std::string; class Website { private: string URL unsigned int IP public: Website() { URL = ""; IP = 0; } };
Class Website has an embedded object of type std::string. The syntax rules of the language do not force the usage of mem-initialization to initialize this member. However, the performance gain in choosing mem-initialization over initialization inside the constructor's body is significant. Why? The initialization inside the constructor's body is very inefficient because it requires the construction of the member URL; a temporary std::string object is then constructed from the value "", which is in turn assigned to URL. Finally, the temporary object has to be destroyed. The use of a mem-initialization list, on the other hand, avoids the creation and destruction of a temporary object (the performance implications of mem-initialization lists are discussed in further detail in Chapter 12, "Optimizing Your Code").
Due to the performance difference between the two forms of initializing embedded objects, some programmers use mem-initialization exclusively -- even for fundamental types. It is important to note, however, that the order of the initialization list has to match the order of declarations within the class. This is because the compiler transforms the list so that it coincides with the order of the declaration of the class members, regardless of the order specified by the programmer. For example
class Website { private: string URL; //1 unsigned int IP; //2 public: Website() : IP(0), URL("") {} // initialized in reverse order };
In the mem-initialization list, the programmer first initializes the member IP, and then URL, even though IP is declared after URL. The compiler transforms the initialization list to the order of the member declarations within the class. In this case, the reverse order is harmless. When there are dependencies in the order of initialization list, however, this transformation can cause unexpected surprises. For example
class string { private: char *buff; int capacity; public: explicit string(int size) : capacity(size), buff (new char [capacity]) {} undefined behavior };
The mem-initialization list in the constructor of string does not follow the order of declaration of string's members. Consequently, the compiler transforms the list into
explicit string(int size) : buff (new char [capacity]), capacity(size) {}
The member capacity specifies the number of bytes that new has to allocate; but it has not been initialized. The results in this case are undefined. There are two ways to avert this pitfall: Change the order of member declarations so that capacity is declared before buff, or move the initialization of buff into the constructor's body.
An implicitly-declared default constructor has an exception specification (exception specifications are discussed in Chapter 6, ""Exception Handling""). The exception specification of an implicitly-declared default constructor contains all the exceptions of every other special member function that the constructor invokes directly. For example
struct A { A(); //can throw any type of exception }; struct B { B() throw(); //not allowed to throw any exceptions }; struct C : public B { //implicitly-declared C::C() throw; } struct D: public A, public B { //implicitly-declared D::D(); };
The implicitly-declared constructor in class C is not allowed to throw any exceptions because it directly invokes the constructor of class B, which is not allowed to throw any exceptions either. On the other hand, the implicitly-declared constructor in class D is allowed to throw any type of exception because it directly invokes the constructors of classes A and B. Since the constructor of class A is allowed to throw any type of exception, D's implicitly-declared constructor has a matching exception specification. In other words, D's implicitly-declared constructor allows all exceptions if any function that it directly invokes allows all exceptions; it allows no exceptions if every function that it directly invokes allows no exceptions either. As you will see soon, the same rules apply to the exception specifications of other implicitly-declared special member functions.
A copy constructor is used to initialize its object with another object. A constructor of a class C is a copy constructor if its first argument is of type C&, const C&, volatile C&, or const volatile C&, and if there are no additional arguments or if all other arguments have default values. If there is no user-defined copy constructor for a class, the implementation implicitly declares one. An implicitly-declared copy constructor is an inline public member of its class, and it has the form
C::C(const C&);
if each base class of C has a copy constructor whose first argument is a reference to a const object of the base class type, and if all the nonstatic embedded objects in C also have a copy constructor that takes a reference to a const object of their type. Otherwise, the implicitly-declared copy constructor is of the following type:
C::C(C&);
An implicitly-declared copy constructor has an exception specification. The exception specification contains all the exceptions that might be thrown by other special functions that the copy constructor invokes directly.
A copy constructor is said to be trivial if it is implicitly declared, if its class has no virtual member functions and no virtual base classes, and if its entire direct base classes and embedded objects have trivial copy constructors. The implementation implicitly defines an implicitly-declared, nontrivial copy constructor to initialize an object of its type from a copy of an object of its type (or from one derived from it). The implicitly-defined copy constructor performs a memberwise copy of its subobjects, as in the following example:
#include<string> using std::string; class Website //no user-defined copy constructor { private: string URL; unsigned int IP; public: Website() : IP(0), URL("""") {} }; int main () { Website site1; Website site2(site1); //invoke implicitly-defined copy constructor }
The programmer did not declare a copy constructor for class Website. Because Website has an embedded object of type std::string, which happens to have a user-defined copy constructor, the implementation implicitly defines a copy constructor for class Website and uses it to copy construct the object site2 from site1. The synthesized copy constructor first invokes the copy constructor of std::string, and then performs a bitwise copying of the data members of site1 into site2.
Novices are sometimes encouraged to define the four special member functions for every class they write. As can be seen in the case of the Website class, not only is this unnecessary, but it is even undesirable under some conditions. The synthesized copy constructor (and the assignment operator, as you are about to see) already do the "right thing". They automatically invoke the constructors of base and member subobjects, they initialize the virtual pointer (if one exists), and they perform a bitwise copying of fundamental types. In many cases, this is exactly the programmer's intention anyway. Furthermore, the synthesized constructor and copy constructor enable the implementation to create code that is more efficient than user-written code because it can apply optimizations that are not always possible otherwise.
Like ordinary constructors, copy constructors -- either implicitly-defined or user-defined -- are augmented by the compiler, which inserts additional code into them to invoke the copy constructors of direct base classes and embedded objects. It is guaranteed, however, that virtual base subobjects are copied only once.
Unlike ordinary member functions, a constructor has to know the exact type of its object at compile time in order to construct it properly. Consequently, a constructor cannot be declared virtual. Still, creating an object without knowing its exact type is useful in certain conditions. The easiest way to simulate virtual construction is by defining a virtual member function that returns a constructed object of its class type. For example
class Browser { public: Browser(); Browser( const Browser&); virtual Browser* construct() { return new Browser; } //virtual default constructor virtual Browser* clone() { return new Browser(*this); } //virtual copy constructor virtual ~Browser(); //... }; class HTMLEditor: public Browser { public: HTMLEditor (); HTMLEditor (const HTMLEditor &); HTMLEditor * construct() { return new HTMLEditor; }//virtual default constructor HTMLEditor * clone() { return new HTMLEditor (*this); } //virtual copy constructor virtual ~HTMLEditor(); //... };
The polymorphic behavior of the member functions clone() and construct() enables -you to instantiate a new object of the right type, without having to know the exact type of the source object.
void create (Browser& br) { br.view(); Browser* pbr = br.construct(); //...use pbr and br delete pbr; }
pbr is assigned a pointer to an object of the right type -- either Browser or any class publicly derived from it. Note that the object br does not delete the new object it has created; this is the user's responsibility. If it did, the lifetime of the reproduced objects would depend on the lifetime of their originator -- which would significantly compromise the usability of this technique.
The implementation of virtual constructors relies on a recent modification to C++, namely virtual functions' covariance. An overriding virtual function has to match the signature and the return type of the function it overrides. This restriction was recently relaxed to enable the return type of an overriding virtual function to co-vary with its class type. Thus, the return type of a public base can be changed to the type of a derived class. The covariance applies only to pointers and references.
CAUTION: Please note that some compilers do not support virtual member functions' covariance yet.
A user-declared assignment operator of class C is a nonstatic, nontemplate member function of its class, taking exactly one argument of type C, C&, const C&, volatile C&, or const volatile C&.
If there is no user-defined assignment operator for a class, the implementation implicitly declares one. An implicitly-declared assignment operator is an inline public member of its class, and it has the form
C& C::operator=(const C&);
if each base class of C has an assignment operator whose first argument is a reference to a const object of base class type, and if all the nonstatic embedded objects in C also have an assignment operator that takes a reference to a const object of their type. Otherwise, the implicitly-declared assignment operator is of the following type:
C& C::operator=(C&);
An implicitly-declared assignment operator has an exception specification. The exception specification contains all the exceptions that might be thrown by other special functions that the assignment operator invokes directly. An assignment operator is said to be trivial if it is implicitly declared, if its class has no virtual member functions or virtual base classes, and if its direct base classes and embedded objects have a trivial assignment operator.
Because an assignment operator is implicitly declared for a class if it is not declared by the programmer, the assignment operator of a base class is always hidden by the assignment operator of a derived class. In order to extend -- rather than override -- the assignment operator in a derived class, you must first invoke the assignment operator of the base explicitly, and then add the operations that are required for the derived class. For example
class B { private: char *p; public: enum {size = 10}; const char * Getp() const {return p;} B() : p ( new char [size] ) {} B& operator = (const C& other); { if (this != &other) strcpy(p, other.Getp() ); return *this; } }; class D : public B { private: char *q; public: const char * Getq() const {return q;} D(): q ( new char [size] ) {} D& operator = (const D& other) { if (this != &other) { B::operator=(other); //first invoke base's assignment operator explicitly strcpy(q, (other.Getq())); //add extensions here } return *this; } };
The synthesized copy constructor and assignment operator perform a memberwise copy. This is the desirable behavior for most uses. However, it can be disastrous for classes that contain pointers, references, or handles. In such cases, you have to define a copy constructor and assignment operator to avoid aliasing. Aliasing occurs when the same resource is used simultaneously by more than one object. For example
#include <cstdio> using namespace std; class Document { private: FILE *pdb; public: Document(const char *filename) {pdb = fopen(filename, "t");} Document(FILE *f =NULL) : pdb{} ~Document() {fclose(pdb);} //bad, no copy constructor //or assignment operator defined }; void assign(Document& d) { Document temp("letter.doc"); d = temp; //Aliasing; both d and temp are pointing to the same file }//temp's destructor is now called and closes file while d is still using it int main() { Document doc; assign(doc); return 0; //doc now uses a file which has just been closed. disastrous }}//OOPS! doc's destructor is now invoked and closes 'letter.doc' once again
Because the implementer of class Document did not define a copy constructor and assignment operator, the compiler defined them implicitly. However, the synthesized copy constructor and assignment operator result in aliasing. An attempt to open or close the same file twice yields undefined behavior. One way to solve this problem is to define an appropriate copy constructor and assignment operator. Please note, however, that the aliasing results from the reliance on low-level language constructs (file pointers in this case), whereas an embedded fstream object can perform the necessary checks automatically. In that case, a user-written copy constructor and assignment operator are unnecessary. The same problem occurs when bare pointers to char are used as data members instead of as string objects. If you use a pointer to char rather than std::string in class Website, you face an aliasing problem as well.
Another conclusion that can be drawn from the preceding example is that whenever you define a copy constructor, you must also define the assignment operator. When you define only one of the two, the compiler creates the missing one -- but it might not work as expected.
The "Big Three Rule" or the "Big Two Rule"?The famous "Big Three Rule" says that if a class needs any of the Big Three member functions (copy constructor, assignment operator, and destructor), it needs them all. Generally, this rule refers to classes that allocate memory from the free store. However, many other classes require only that the Big Two (copy constructor and assignment operator) be defined by the user; the destructor, nonetheless, is not always required. Examine the followingexample:
class Year { private: int y; bool cached; //has the object been cached? public: //... Year(int y); Year(const Year& other) //cached should not be copied { y = other.getYear(); } Year& operator =(const Year&other) //cached should not be copied { y = other.getYear(); return *this; } int getYear() const { return y; } };//no destructor required for class YearClass Year does not allocate memory from the free store, nor does it acquire any other resources during its construction. A destructor is therefore unnecessary. However, the class needs a user-defined copy constructor and assignment operator to ensure that the value of the member that is cached is not copied because it is calculated for every individual object separately.
When a user-defined copy constructor and assignment operator are needed, it is important to implement them in a way that prevents self-assignment or aliasing. Usually, it is sufficient to fully implement only one of the two, and then define the other by means of the first. For example
#include <cstring> using namespace std; class Person { private: int age; char * name; public: int getAge () const { return age;} const char * getName() const { return name; } //... Person (const char * name = NULL, int age =0) {} Person & operator= (const Person & other); Person (const Person& other); }; Person & Person::operator= (const Person & other) { if (&other != this) //guard from self assignment { size_t len = strlen( other.getName()); if (strlen (getName() ) < len) { delete [] name; //release current buffer name = new char [len+1]; } strcpy(name, other.getName()); age = other.getAge(); } return *this; } Person::Person (const Person & other) { *this=other; //OK, use user-defined assignment operator is invoked }
There are situations in which enabling the user to copy or assign a new value to an object is undesirable. You can disable both by explicitly declaring the assignment operator and copy constructor as private:
class NoCopy { private: NoCopy& operator = (const NoCopy& other) { return *this; } NoCopy(const NoCopy& other) {/*..*/} public: NoCopy() {} //... }; void f() { NoCopy nc; // fine, default constructor called NoCopy nc2(nc); //error; attempt to call a private copy constructor nc2 = nc; //also a compile time error; operator= is private }
A destructor destroys an object of its class type. It takes no arguments and has no return type (not even void). const and volatile qualities are not applied on an object under destruction; therefore, destructors can be invoked for const, volatile, or const volatile objects. If there is no user-defined destructor for a class, the implementation implicitly declares one. An implicitly-declared destructor is an inline public member of its class and has an exception specification. The exception specification contains all the exceptions that might be thrown by other special functions that the destructor invokes directly.
A destructor is trivial if it is implicitly declared and if its entire direct base classes and embedded objects have trivial destructors. Otherwise, the destructor is nontrivial. A destructor invokes the destructors of the direct base classes and member objects of its class. The invocation occurs in the reverse order of their construction. All destructors are called with their qualified name, ignoring any possible virtual overriding destructors in more derived classes. For example
#include <iostream> using namespace std; class A { public: virtual ~A() { cout<<"destroying A"<<endl;} }; class B: public A { public: ~B() { cout<<"destroying B"<<endl;} }; int main() { B b; return 0; };
This program displays
destroying B destroying A
This is because the compiler transforms the user-defined destructor of class B into
~B() { //user-written code below cout<<"destroying B"<<endl; //pseudo C++ code inserted by the compiler below this->A::~A(); // destructor called using its qualified name }
Although the destructor of class A is virtual, the qualified call that is inserted into the destructor of class B is resolved statically (calling a function with a qualified name bypasses the dynamic binding mechanism).
Destructors are invoked implicitly in the following cases:
For static objects at program termination
For local objects when the block in which the object is created exits
For a temporary object when the lifetime of the temporary object ends
For objects allocated on the free store using new, through the use of delete
During stack unwinding that results from an exception
A destructor can also be invoked explicitly. For example:
class C { public: ~C() {} }; void destroy(C& c) { c.C::~C(); //explicit destructor activation }
A destructor can also be explicitly invoked from within a member function of its object:
void C::destroy() { this->C::~C(); }
In particular, explicit destructor invocation is necessary for objects that were created by the placement new operator (placement new is discussed in Chapter 11, "Memory Management").
Fundamental types have constructors, as you have seen. In addition, fundamental types also have a pseudo destructor. A pseudo destructor is a syntactic construct whose sole purpose is to satisfy the need of generic algorithms and containers. It is a no-op code that has no real effect on its object. If you examine the assembly code that your compiler produces for a pseudo destructor invocation, you might discover that the compiler simply ignored it. A pseudo destructor invocation is shown in the following example:
typedef int N; int main() { N i = 0; i.N::~N(); //pseudo destructor invocation i = 1; //i was not affected by the invocation of the pseudo destructor return 0; }
The variable i is defined and initialized. In the following statement, the pseudo destructor of the non-class type N is explicitly invoked but it has no effect on its object. Like the constructors of fundamental types, pseudo destructors enable the user to write code without having to know if a destructor actually exists for a given type.
Unlike ordinary member functions, a virtual destructor is not overridden when it is redefined in a derived class. Rather, it is extended. The lower-most destructor first invokes the destructor of its base class; only then is it executed. Consequently, when you try to declare a pure virtual destructor, you might encounter compilation errors, or worse -- a runtime crash. In this respect, pure virtual destructors are exceptional among pure virtual functions -- they have to be defined. You can refrain from declaring a destructor with the pure specifier, making it only virtual. However, this is an unnecessary design compromise. You can enjoy both worlds by forcing an interface whose members are all pure virtual, including the destructor -- and all this without experiencing runtime crashes. How is it done?
First, the abstract class contains only a declaration of a pure virtual destructor:
class Interface { public: virtual void Open() = 0; virtual ~Interface() = 0; };
Somewhere outside the class declaration, the pure virtual destructor has to be defined as follows:
Interface::~Interface() {} //definition of a pure virtual destructor; should always be empty
When you are designing a class, remember that it might serve as a base for other subclasses. It can also be used as a member object in a larger class. As opposed to ordinary member functions, which can be overridden or simply not called, the base class constructor and destructor are automatically invoked. It is not a good idea to force users of a derived and embedded object to pay for what they do not need, but are forced to accept. In other words, constructors and destructors should contain nothing but the minimal functionality needed to construct an object and destroy it. A concrete example can demonstrate that: A string class that supports serialization should not open/create a file in its constructor. Such operations need to be left to a dedicated member function. When a new derived class -- such as ShortString, which holds a fixed length string -- is created, its constructor is not forced to perform superfluous file I/O that is imposed by the constructor of its base class.
The constructor, copy constructor, assignment operator, and destructor automate most of the tedium that is associated with creating, copying, and destroying objects. The symmetry between a constructor and a destructor in C++ is rare among object-oriented programming languages, and it serves as the basis for advanced design idioms (as you will see in the next chapter, "Object Oriented Programming and Design").
Each C++ object possesses the four member functions, which can be declared by the programmer or declared implicitly by the implementation. An implicitly-declared special member function can be trivial, which means that the implementation does not have to define it. The synthesized special member functions perform only operations that are required by the implementation. User-written special member functions are automatically augmented by the compiler -- to ensure the proper initialization of base and embedded subobjects -- and the virtual pointer. Fundamental types have constructors and pseudo destructors, which facilitate generic programming.
In many cases, the synthesized special member functions do the "right thing". When the default behavior is unfitted, the programmer has to define one or more of the special functions explicitly. Often, however, the need for user-written code derives from combining low-level data structures with a high-level interface, and might indicate a design flaw. Declaring a constructor explicit ensures that it will not serve as an implicit conversion operator.
A mem-initialization list is necessary for the initialization of const and reference data members, as well as to pass arguments to a base or embedded subobject. In all other cases, a mem-initialization list is optional but can enhance performance. Constructors and assignment operators can be used in several ways to control instantiation and copying of objects. Destructors can be invoked explicitly. Destructors that are declared pure virtual have to be defined.
© Copyright 1999, Macmillan Computer Publishing. All rights reserved.