📄 ch12.htm
字号:
rather than initialization:</p><pre><tt>void func2() //less efficient than func()? Not necessarily</tt><tt>{</tt><tt> string s;</tt><tt> bool emp;</tt><tt> emp = s.empty(); //late assignment</tt><tt>}</tt></pre><p>My compiler produces the same assembly code as it did with the initialization version. However, as far as user-defined types are concerned, the difference between initialization and assignment can be quite noticeable. The following example demonstrates the performance gain in this case (by modifying the preceding example). Instead of a <tt>bool</tt> variable, a full-blown class object is used, which has all the four special member functions defined:</p><pre><tt>int constructor, assignment_op, copy, destr; //global counters</tt><tt>class C</tt><tt>{</tt><tt>public:</tt><tt> C();</tt><tt> C& operator = (const C&);</tt><tt> C(const C&);</tt><tt> ~C();</tt><tt>};</tt><tt>C::C() </tt><tt>{</tt><tt> ++constructor;</tt><tt>}</tt><tt>C& C::operator = (const C& other)</tt><tt>{</tt><tt> ++assignment_op;</tt><tt> return *this;</tt><tt>}</tt><tt>C::C(const C& other)</tt><tt>{</tt><tt> ++copy;</tt><tt>}</tt><tt>C::~C()</tt><tt>{</tt><tt> ++destr;</tt><tt>} </tt></pre><p>As in the previous example, two different versions of the same function are compared; the first uses object initialization and the second uses assignment:</p><pre><tt>void assign(const C& c1)</tt><tt>{</tt><tt> C c2;</tt><tt> c2 = c1;</tt><tt>}</tt><tt>void initialize(const C& c1)</tt><tt>{</tt><tt> C c2 = c1;</tt><tt>}</tt></pre><p>Calling <tt>assign()</tt> causes three member function invocations: one for the constructor, one for the assignment operator, and one for the destructor. <tt>initialize()</tt> causes only two member function invocations: the copy constructor and the destructor. Initialization saves one function call. For a nonsensical class such as <tt>C</tt>, the additional runtime penalty that results from a superfluous constructor call might not be crucial. However, bear in mind that constructors of real-world objects also invoke constructors of their base classes and embedded objects. When there is a choice between initialization and assignment, therefore, initialization is always preferable.</p><h3> <a name="Heading6"> Relocating Declarations</a></h3><p>Preferring initialization of objects over assignment is one aspect of localizing declarations. On some occasions, the performance boost that can result from moving declarations is even more appreciable. Consider the following example:</p><pre><tt>bool is_C_Needed(); </tt><tt>void use()</tt><tt>{</tt><tt> C c1;</tt><tt> if (is_C_Needed() == false)</tt><tt> {</tt><tt> return; //c1 was not needed</tt><tt> } </tt><tt> //use c1 here</tt><tt> return; </tt><tt>}</tt></pre><p>The local object <tt>c1</tt> is unconditionally constructed and destroyed in <tt>use()</tt>, even if it is not used at all. The compiler transforms the body of <tt>use()</tt> into something that looks like this: </p><pre><tt>void use()</tt><tt>{</tt><tt> C c1;</tt><tt> c1.C::C(); //1. compiler-added constructor call</tt><tt> if (is_C_Needed() == false)</tt><tt> {</tt><tt> c1.C::~C(); //2. compiler-added destructor call</tt><tt> return; //c1 was not needed but was constructed and destroyed still</tt><tt> } </tt><tt> //use c1 here</tt><tt> c1.C::~C(); //3. compiler-added destructor call</tt><tt> return; </tt><tt>}</tt></pre><p>As you can see, when <tt>is_C_Needed()</tt> returns <tt>false</tt>, the unnecessary construction and destruction of <tt>c1</tt> are still unavoidable. Can a clever compiler optimize away the unnecessary construction and destruction in this case? The Standard allows the compiler to suppress the creation (and consequently, the destruction) of an object if it is not needed, and if neither its constructor nor its destructor has any side effects. In this example, however, the compiler cannot perform this feat for two reasons. First, both the constructor and the destructor of <tt>c1</tt> have side effects -- they increment counters. Second, the result of <tt>is_C_Needed()</tt> is unknown at compile time; therefore, there is no guarantee that <tt>c1</tt> is actually unnecessary at runtime. Nevertheless, with a little help from the programmer, the unnecessary construction and destruction can be eliminated. All that is required is the relocation of the declaration of <tt>c1</tt> to the point where it is actually used:</p><pre><tt>void use()</tt><tt>{</tt><tt> if (is_C_Needed() == false)</tt><tt> {</tt><tt> return; //c1 was not needed</tt><tt> } </tt><tt> C c1; //moved from the block's beginning</tt><tt> //use c1 here</tt><tt> return; </tt><tt>}</tt></pre><p>Consequently, the object <tt>c1</tt> is constructed only when it is really needed -- that is, when <tt>is_C_Needed()</tt> returns <tt>true</tt>. On the other hand, if <tt>is_C_Needed()</tt> returns <tt>false</tt>, <tt>c1</tt> is neither constructed nor destroyed. Thus, simply by moving the declaration of <tt>c1</tt>, you managed to eliminate two unnecessary member function calls! How does it work? The compiler transforms the body of <tt>use()</tt> into something such as the following: </p><pre><tt>void use()</tt><tt>{</tt><tt> if (is_C_Needed() == false)</tt><tt> {</tt><tt> return; //c1 was not needed</tt><tt> } </tt><tt> C c1; //moved from the block's beginning</tt><tt> c1.C::C(); //1 compiler-added constructor call</tt><tt> //use c1 here</tt><tt> c1.C::~C(); //2 compiler-added destructor call</tt><tt> return; </tt><tt>}</tt></pre><p>To realize the effect of this optimization, change the body of <tt>use()</tt>. Instead of constructing a single object, you now use an array of 1000 <tt>C</tt> objects:</p><pre><tt>void use()</tt><tt>{</tt><tt> if (is_C_Needed() == false)</tt><tt> {</tt><tt> return; //c1 was not needed</tt><tt> } </tt><tt> C c1[1000]; </tt><tt> //use c1 here</tt><tt> return; </tt><tt>}</tt></pre><p>In addition, you define <tt>is_C_Needed()</tt> to return <tt>false</tt>: </p><pre><tt>bool is_C_Needed() </tt><tt>{ </tt><tt> return false;</tt><tt>}</tt></pre><p>Finally, the <tt>main()</tt> driver looks similar to the following:</p><pre><tt>int main()</tt><tt>{ </tt><tt> for (int j = 0; j<100000; j++)</tt><tt> use();</tt><tt> return 0; </tt><tt>}</tt></pre><p>The two versions of <tt>use()</tt> differ dramatically in their performance. They were compared on a Pentium II, 233MHz machine. To corroborate the results, the test was repeated five times. When the optimized version was used, the <tt>for</tt> loop in <tt>main()</tt> took less than 0.02 of a second, on average. However, when the same <tt>for</tt> loop was executed with the original, the nonoptimized version of <tt>use()</tt> took 16 seconds. The dramatic variation in these results isn't too surprising; after all, the nonoptimized version incurs 100,000,000 constructor calls as well as 100,000,000 destructor calls, whereas the optimized version calls none. These results might also hint at the performance gain that can be achieved simply by preallocating sufficient storage for container objects, rather than allowing them to reallocate repeatedly (see also Chapter 10, "STL and Generic Programming"). </p><h3> <a name="Heading7"> Member-Initialization Lists</a></h3><p>As you read in Chapter 4, "Special Member Functions: Default Constructor, Copy Constructor, Destructor, and Assignment Operator," a member initialization list is needed for the initialization of <tt>const</tt> and reference data members, and for passing arguments to a constructor of a base or embedded subobject. Otherwise, data members can either be assigned inside the constructor body or initialized in a member initialization list. For example</p><pre><tt>class Date //mem-initialization version</tt><tt>{</tt><tt>private:</tt><tt> int day;</tt><tt> int month;</tt><tt> int year;</tt><tt> //constructor and destructor</tt><tt>public:</tt><tt> Date(int d = 0, int m = 0, int y = 0) : day , month(m), year(y) {}</tt><tt>};</tt></pre><p>Alternatively, you can define the constructor as follows:</p><pre><tt>Date::Date(int d, int m, int y) //assignment within the constructor body</tt><tt>{</tt><tt> day = d; </tt><tt> month = m; </tt><tt> year = y;</tt><tt>}</tt></pre><p>Is there a difference in terms of performance between the two constructors? Not in this example. All the data members in <tt>Date</tt> are of a fundamental type. Therefore, initializing them by a mem-initialization list is identical in terms of performance to assignment within the constructor body. However, with user-defined types, the difference between the two forms is significant. To demonstrate that, return to the member function counting class, <tt>C</tt>, and define another class that contains two instances thereof:</p><pre><tt>class Person</tt><tt>{</tt><tt>private:</tt><tt> C c_1;</tt><tt> C c_2;</tt><tt>public:</tt><tt> Person(const C& c1, const C& c2 ): c_1(c1), c_2(c2) {}</tt><tt>};</tt></pre><p>An alternative version of <tt>Person</tt>'s constructor looks similar to the following:</p><pre><tt>Person::Person(const C& c1, const C& c2)</tt><tt>{</tt><tt> c_1 = c1; </tt><tt> c_2 = c2; </tt><tt>}</tt></pre><p>Finally, the <tt>main()</tt> driver is defined as follows:</p><pre><tt>int main()</tt><tt>{</tt><tt> C c; //created only once, used as dummy arguments in Person's constructor </tt><tt> for (int j = 0; j<30000000; j++)</tt>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -