statement name =s.assigns the char pointer s to the left-hand side object of type string.The tThe computation the is thrown away in its entirety. rewrite he constructor implementation.caming back the lost speed and trading away nothing You achieve that by specifying an explicit initialization of thesng member name. Person:Person(const char *s):name (s)()//Version 1. This constructor will generate the exact same Person object with the exception of improved performance All we get here is a single call(const char to initialize name with s.We tested the performance difference by timing the following loop: for (i =0: MILLION;++)( Person p("Pele"); The chart in Figure 22 compares the execution time of Version(silent initialization plus explicit assignment)to that of Version 1(explicit initialization only). Figure 2.2.Overhead of a silent initialization is negligible in this particular scenario 1,500 1,400 1350 1,000 500 0 o V1 u decided,for some reason.to bypass the standard string implementation and roll your own instead.We'll call it superstrng class Superstring public:
22 statement name = s; assigns the char pointer s to the left-hand side object of type string. The string::operator=(const char*) is invoked to execute that assignment. The computation performed by the assignment operator essentially overwrites the result of the default string constructor invoked earlier during initialization. The computational effort contained in the default string constructor is thrown away in its entirety. Many optimizations require some kind of a trade-off. You often trade speed for clarity, simplicity, reusability, or some other metric. But in this example, optimization requires no sacrifice at all. You can rewrite the Person constructor implementation, claiming back the lost speed and trading away nothing. You achieve that by specifying an explicit initialization of the string member name. Person::Person(const char *s) : name(s) {} // Version 1. This constructor will generate the exact same Person object with the exception of improved performance. All we get here is a single call to string::string(const char *) to initialize name with s. We tested the performance difference by timing the following loop: for (i = 0; i < MILLION; i++) { Person p("Pele"); } The chart in Figure 2.2 compares the execution time of Version 0 (silent initialization plus explicit assignment) to that of Version 1 (explicit initialization only). Figure 2.2. Overhead of a silent initialization is negligible in this particular scenario. Fortunately, the default string constructor is very cheap, making this inefficiency almost negligible. You are not always going to be that lucky, so don't count on it. Suppose you decided, for some reason, to bypass the standard string implementation and roll your own instead. We'll call it SuperString: class SuperString { public: SuperString(const char *s = 0); SuperString(const SuperString &s);
SuperString&operator-(const Superstring&s); -Superstring()(delete [str;) private: char *stri a if (s 1-0) char[strlen(s)+1]; strcpy(str,s); Supestng:supesting (conet Superstring if (s.str) Superstrings Superstring::operator-(const Superstrings s) if (str if (s.str)( cpy(en() else str-0; return *this; Furtherm Person (const char *s)(name =s;)//Version 2 statement name -s; 3
23 SuperString& operator=(const SuperString& s); ~SuperString() {delete [] str;} private: char *str; }; inline SuperString::SuperString(const char *s) : str(0) { if (s != 0) { str = new char[strlen(s)+1]; strcpy(str,s); } } inline SuperString::SuperString(const SuperString &s) : str(0) { if (s.str) { str = new char[strlen(s.str)+1]; strcpy(str,s.str); } } SuperString& SuperString::operator=(const SuperString& s) { if (str != s.str) { delete [] str; if (s.str) { str = new char[strlen(s.str)+1]; strcpy(str,s.str); } else str = 0; } return *this; } Furthermore, you have decided to replace the string member with a SuperString in your Person implementation: class Person { public: Person (const char *s) { name = s; }// Version 2 ... private: SuperString name; }; Because you did not provide a SuperString assignment operator that accepts a char pointer argument, the assignment operator demands a SuperString object reference as an argument. Consequently, the statement name = s;
in theeson constructor will trigger the creation of a temporary Supersting object.The compiler will convert th impmemcmatoa ng pseu owing transformation e Person:Person (char*) Person:Person(const char 's) Initialueing::upting( /Constructor: /member "name". superstring_temp /Temporary object supeE8gaeerstring:superstrings: //construct a /object from "s" nameuing:rttemp) //Assign temp to /Destructor for /object. The The conien ofhee olletheon Overall we get two Superstring const r.The performance damage in this implmenation is much more severe(see3).Version Person:Person(const char *s):name(s)() nitiona Figure 2.3.More significant impact of silent initialization. 3,000 2,400 2.500 2,000 1,500 1,150 1,000 500 0
24 in the Person constructor will trigger the creation of a temporary SuperString object. The compiler will convert the char pointer s to a SuperString by invoking the appropriate SuperString constructor. Using pseudocode, we have the following transformation of the Person::Person(char*) implementation Person::Person(const char *s) { name.SuperString::SuperString(); // Constructor: Initialize // member "name". SuperString _temp; // Temporary object. _temp.SuperString::SuperString(s); // Construct a SuperString // object from "s". name.SuperString::operator=(_temp); // Assign _temp to "name". _temp.SuperString::~SuperString(); // Destructor for temporary // object. } The content of the temporary SuperString object (temp) is assigned to member name via invocation of the SuperString assignment operator. It is then followed by invocation of the SuperString destructor to tear down the temporary object. Overall we get two SuperString constructors, one call to the assignment operator and another to the destructor. The performance damage in this implementation is much more severe (see Figure 2.3). Version 3 fixes it by explicit initialization of the SuperString member: Person::Person(const char *s) : name(s) {} // Version 3. Explicit // initialization Figure 2.3. More significant impact of silent initialization
Version2 is the one using both silent initialization plus an explicit assignment in the constructor body and no assig the case o om-grown severe had an assignment operator been provided that takes a char pointer as an argument.That would have eliminated the need for a temporary supesting object.It is interesting to note that Version 3 is a ittle faster than the corresponding version uing the compiler implem go假的oc无西险s日 Key Points .Constructors and destructors may be as efficient as hand-crafted C code.In practice,however. they often contain overhead in the form of superfluous computations. The con on (de They make construction and destruction more expensive encapsulated black-box entities and scourages you from l oking inside.How do we balance ensitive the black-bo ak havoc on the20%that is performance critical.It is also application dependent Some The object life and de ant to defer object to the scope n which it is manipulaed Compilers must nitialize conta ained member r body.You overhead of calling the ass also avoid the generation of temporary objects. 25
25 Version 2 is the one using both silent initialization plus an explicit assignment in the constructor body. Version 3 is the one doing an explicit initialization and no assignment. In the case of our home-grown SuperString, Version 3 is more than twice as fast. The performance damage would have been less severe had an assignment operator been provided that takes a char pointer as an argument. That would have eliminated the need for a temporary SuperString object. It is interesting to note that Version 3 is a little faster than the corresponding version using the compiler string implementation (Version 1). Our home-grown SuperString does not provide anything near the rich functionality of the string class. It is often the case that code runs faster if it does not have to provide much flexibility. Key Points • Constructors and destructors may be as efficient as hand-crafted C code. In practice, however, they often contain overhead in the form of superfluous computations. • The construction (destruction) of an object triggers recursive construction (destruction) of parent and member objects. Watch out for the combinatorial explosion of objects in complex hierarchies. They make construction and destruction more expensive. • Make sure that your code actually uses all the objects that it creates and the computations that they perform. We would encourage people to peer inside the classes that they use. This advice is not going to be popular with OOP advocates. OOP, after all, preaches the use of classes as encapsulated black-box entities and discourages you from looking inside. How do we balance between those competing pieces of advice? There is no simple answer because it is context sensitive. Although the black-box approach works perfectly well for 80% of your code, it may wreak havoc on the 20% that is performance critical. It is also application dependent. Some application will put a premium on maintainability and flexibility, and others may put performance considerations at the top of the list. As a programmer you are going to have to decide the question of what exactly you are trying to maximize. • The object life cycle is not free of cost. At the very least, construction and destruction of an object may consume CPU cycles. Don't create an object unless you are going to use it. Typically, you want to defer object construction to the scope in which it is manipulated. • Compilers must initialize contained member objects prior to entering the constructor body. You ought to use the initialization phase to complete the member object creation. This will save the overhead of calling the assignment operator later in the constructor body. In some cases, it will also avoid the generation of temporary objects
Chapter 3.Virtual Functions The evoltion of pronnguetenddthe pr ask easier by shifting burden from the p grammer to the co .maintain, The prol em with this progress is Virtual Function Mechanics oid virtual functio uld emulate idin ZooAnimal is your base class: virtual void draw() veype()) int myType: The rest of the animals in the zoo are derived from ZooAnimal.The resolveType()method will enable you to distinguish a Bear from a Monkey at run-time. class Bear public ZooAnimal( public: st char *name):myName(name),myType(BEAR)() void draw(); 1: Each animal sets its appropriate type in its constructor. If you wanted to draw all animals in the oo.you would end up with something along the lines of [Lip] void drawAilAnimais (ooAnimal "p)te to first anima in the for (2ooAnimal "p-p2;p ip-p->next)( case BEAR: ))da() case MONKEY p)->draw () 26
26 Chapter 3. Virtual Functions The evolution of programming languages tended to make the programming task easier by shifting some burden from the programmer to the compiler, interpreter, assembler, or linker. Programs are becoming easier to develop, maintain, and extend. The problem with this progress is that it is often a zero-sum game where gains in one area mean losses in another. In particular, advances in programming often translate into loss of raw speed. Dynamic binding of function calls is one among many contributions C++ has made to C programming. It shifts the burden of type resolution from the programmer to the compiler, which is good. On the other hand, it can have a negative impact on cost, which is what we intend to examine. Virtual Function Mechanics If you really wanted to avoid virtual functions, you could emulate dynamic binding by providing your own type resolution code. Suppose you are maintaining a class hierarchy of zoo animals [Lip91], where ZooAnimal is your base class: class ZooAnimal { public: ... virtual void draw(); int resolveType() {return myType;} private: int myType; ... } The rest of the animals in the zoo are derived from ZooAnimal. The resolveType() method will enable you to distinguish a Bear from a Monkey at run-time. class Bear : public ZooAnimal { public: Bear (const char *name) : myName(name), myType(BEAR) {} void draw(); ... }; Each animal sets its appropriate type in its constructor. If you wanted to draw all animals in the zoo, you would end up with something along the lines of [Lip91]: void drawAllAnimals (ZooAnimal *pz) // pointer to first animal in the // list { for (ZooAnimal *p=pz; p ;p = p->next) { switch (p->resolveType()) { case BEAR: ( (Bear *) p)->draw(); break; case MONKEY: ((Monkey *) p)->draw(); break; ... // Handle all other animals currently in the zoo. }