- •Introduction
- •Saving Time with This Book
- •Conventions Used in This Book
- •Part II: Working with the Pre-Processor
- •Part III: Types
- •Part IV: Classes
- •Part V: Arrays and Templates
- •Part VI: Input and Output
- •Part VII: Using the Built-in Functionality
- •Part VIII: Utilities
- •Part IX: Debugging C++ Applications
- •Part X: The Scary (or Fun!) Stuff
- •Icons Used in This Book
- •Creating and Implementing an Encapsulated Class
- •Creating a Mailing-List Application
- •Testing the Mailing-List Application
- •Customizing a Class with Polymorphism
- •Testing the Virtual Function Code
- •Why Do the Destructors Work?
- •Delayed Construction
- •The cDate Class
- •Testing the cDate Class
- •Creating the Header File
- •Testing the Header File
- •The Assert Problem
- •Fixing the Assert Problem
- •Using the const Construct
- •Identifying the Errors
- •Fixing the Errors
- •Fixing What Went Wrong with the Macro
- •Using Macros Appropriately
- •Using the sizeof Function
- •Evaluating the Results
- •Using sizeof with Pointers
- •Implementing the Range Class
- •Testing the Range Class
- •Creating the Matrix Class
- •Matrix Operations
- •Multiplying a Matrix by a Scalar Value
- •Multiplying a Matrix by Scalar Values, Take 2
- •Testing the Matrix Class
- •Implementing the Enumeration Class
- •Testing the Enumeration Class
- •Implementing Structures
- •Interpreting the Output
- •Defining Constants
- •Testing the Constant Application
- •Using the const Keyword
- •Illustrating Scope
- •Interpreting the Output
- •Using Casts
- •Addressing the Compiler Problems
- •Testing the Changes
- •Implementing Member-Function Pointers
- •Updating Your Code with Member-Function Pointers
- •Testing the Member Pointer Code
- •Customizing Functions We Wrote Ourselves
- •Testing the Default Code
- •Fixing the Problem
- •Testing the Complete Class
- •Implementing Virtual Inheritance
- •Correcting the Code
- •Rules for Creating Overloaded Operators
- •Using Conversion Operators
- •Using Overloaded Operators
- •Testing the MyString Class
- •Rules for Implementing new and delete Handlers
- •Overloading new and delete Handlers
- •Testing the Memory Allocation Tracker
- •Implementing Properties
- •Testing the Property Class
- •Implementing Data Validation with Classes
- •Testing Your SSN Validator Class
- •Creating the Date Class
- •Testing the Date Class
- •Some Final Thoughts on the Date Class
- •Creating a Factory Class
- •Testing the Factory
- •Enhancing the Manager Class
- •Implementing Mix-In Classes
- •Testing the Template Classes
- •Implementing Function Templates
- •Creating Method Templates
- •Using the Vector Class
- •Creating the String Array Class
- •Working with Vector Algorithms
- •Creating an Array of Heterogeneous Objects
- •Creating the Column Class
- •Creating the Row Class
- •Creating the Spreadsheet Class
- •Testing Your Spreadsheet
- •Working with Streams
- •Testing the File-Reading Code
- •Creating the Test File
- •Reading Delimited Files
- •Testing the Code
- •Creating the XML Writer
- •Testing the XML Writer
- •Creating the Configuration-File Class
- •Setting Up Your Test File
- •Building the Language Files
- •Creating an Input Text File
- •Reading the International File
- •Testing the String Reader
- •Creating a Translator Class
- •Testing the Translator Class
- •Creating a Virtual File Class
- •Testing the Virtual File Class
- •Using the auto_ptr Class
- •Creating a Memory Safe Buffer Class
- •Throwing and Logging Exceptions
- •Dealing with Unhandled Exceptions
- •Re-throwing Exceptions
- •Creating the Wildcard Matching Class
- •Testing the Wildcard Matching Class
- •Creating the URL Codec Class
- •Testing the URL Codec Class
- •Testing the Rot13 Algorithm
- •Testing the XOR Algorithm
- •Implementing the transform Function to Convert Strings
- •Testing the String Conversions
- •Implementing the Serialization Interface
- •Creating the Buffer Class
- •Testing the Buffer Class
- •Creating the Multiple-Search-Path Class
- •Testing the Multiple-Search-Path Class
- •Testing the Flow Trace System
- •The assert Macro
- •Logging
- •Testing the Logger Class
- •Design by Contract
- •Adding Logging to the Application
- •Making Functions Inline
- •Avoiding Temporary Objects
- •Passing Objects by Reference
- •Choosing Initialization Instead of Assignment
- •Learning How Code Operates
- •Testing the Properties Class
- •Creating the Locking Mechanism
- •Testing the Locking Mechanism
- •Testing the File-Guardian Class
- •Implementing the Complex Class
- •Creating the Conversion Code
- •Testing the Conversion Code
- •A Sample Program
- •Componentizing
- •Restructuring
- •Specialization
- •Index
65 Optimizing
Your Code
Technique
Making functions inline
Avoding temporary objects
Passing by reference
Postponing variable declarations
Choosing initialization over assigment
The final stage in any development process is to optimize the code to make it run better, faster, and more efficiently. Of course, in an ideal world, you would be optimizing the code as you went along,
but for most applications this simply isn’t possible. Instead, developers first get all of the functionality in place, test it, and then optimize it. This technique explores some methods you can use in the post-development phase to optimize your code. While it will never beat developing optimized code in the first place, post-development optimization can still identify and fix many bottlenecks in the code, and give a boost to code that runs just a little too slow.
Making Functions Inline
The first optimization technique that you can use is to make your functions inline code versions. Inline functions and methods are those that are defined at the same time they are implemented, in the class header.
An inline function can be considerably faster than its non-inline brethren, but that speed comes at a cost. Inline functions make your program bigger, and can sometimes even make it slower, because they add overhead to the code. An inline function is expanded in place wherever it is called, much like a macro. This can cause the code to grow significantly, because there are many copies of the same code in the program. It could slow loading and executing of the program down, if there are enough of the copies to make the program very large in size. However, in many cases, you can speed up your code significantly by making accessor functions inline. The reason is that the compiler can optimize the code in place, so that procedures that use inline functions are more optimal than they would be with regular function calls. Accessor functions, which provide read and write access to individual data members, are excellent targets for inlining because they are very small and can be optimized readily.
408 Technique 65: Optimizing Your Code
Take a look at the following class:
class Foo
{
string _str; public:
Foo(const char *str)
{
_str = str;
}
string getString(void);
};
string Foo::getString(void)
{
return _str;
}
}
We can improve the efficiency of this method by inlining the method, like this:
string getString(void)
{
return str;
}
Although many modern compilers do this optimization for you automatically, it’s always up to you to write the best possible code. Don’t rely on the compiler to fix it up.
The rules for inlining are very simple:
Always inline a simple accessor.
Never inline a large method, as the amount of code added to your program far outweighs the savings from the inline.
Inline only when the savings from the function overhead are small compared to the overall savings. In other words, inlining functions that call other functions is generally wasteful.
Avoiding Temporary Objects
If there is a single optimization technique that you should most seriously look at in C++, it’s to avoid having to create and delete temporary objects. Every time you pass an object by value, you make a temporary copy of that object. Every time that you write code that casts a basic data type to an object, you create a temporary object. If you want to optimize your code, look through it and remove all temporary object creations. Let’s take a look at an example of code that really overdoes it with temporaries, to get an idea of what the problem really is.
1. In the code editor of your choice, create a new file to hold the code for the implementation of the source file.
In this example, the file is named ch65.cpp, although you can use whatever you choose.
2. Type the code from Listing 65-1 into your file.
Better yet, copy the code from the source file on this book’s companion Web site.
LISTING 65-1: TEMPORARY OBJECTS
#include <iostream> #include <stdlib.h> #include <time.h>
using namespace std;
class Integer
{
int _iVal; public:
Integer()
{
cout << “Void constructor” << endl; _iVal = 0;
}
Integer( int iVal )
{
Avoiding Temporary Objects 409
cout << “Normal constructor” << endl;
_iVal = iVal;
}
Integer( const Integer& aCopy )
{
cout << “Copy constructor” << endl; _iVal = aCopy._iVal;
}
~Integer()
{
cout << “Destructor” << endl;
}
int getInt() const
{
return _iVal;
}
void setInt(int iVal)
{
_iVal = iVal;
}
Integer operator+= ( const Integer& i1 )
{
setInt( i1.getInt() + getInt() ); return *this;
}
};
Integer operator+( const Integer& i1, const
{ |
Integer& i2 ) |
3 |
|
Integer out; |
1 |
|
out.setInt( i1.getInt() + i2.getInt() ); |
|
} |
return out; |
2 |
Integer operator-( const Integer& i1, const { Integer& i2 ) 4
return Integer( |
i1.getInt() - |
i2.getInt() ); |
|
} |
|
void func( Integer |
i1 ) |
{ |
|
cout << “Integer Value: “ << i1.getInt() << endl;
}
int main(void)
{ |
|
Integer |
i1(5), i2(3); |
Integer |
i3; |
cout << |
“Test plus: “ << endl; |
i3 = i1 |
+ i2; |
cout << |
“Result: “ << i3.getInt() << |
endl; |
|
cout << |
“Test minus: “ << endl; |
Integer |
i4 = i3 - i2; |
cout << |
“Result: “ << i4.getInt() << |
endl; |
|
cout << “Calling function” << endl; func( i4 );
}
If we look at the operator+ function that works with the Integer class, you will see that the function accepts two Integer objects by reference. No objects are created here. However, within the object, we create a temporary object that is created locally (see 1). This object is then used to set the pieces of the integer from the two
Integer objects passed into the function, before |
|
being returned to the calling program at |
2. |
The result is then assigned to the result variable |
in the main program (see 3). How many tem- |
|
porary objects are being created |
here? Let’s run |
the program and find out.
3. Save the source file in the code editor and then close the editor application.
4. Compile the source-code file with your favorite compiler on your favorite operating system.
5. Run the program on the console window of your favorite operating system.
410 Technique 65: Optimizing Your Code
If you have done everything correctly, you should see the following output on the console window:
$ ./a.exe
Normal constructor Normal constructor Void constructor Test plus:
Void constructor Destructor Result: 8
Test minus: Normal constructor Result: 5
Calling function Copy constructor Integer Value: 5 Destructor Destructor Destructor Destructor Destructor
The output shows each time an Integer object is being created and destroyed. Take a look at the print statements, especially those between the Test plus: statement and the Test minus: statement. There is a temporary object created here, and then destroyed.
Look at the difference between the addition operator (shown in Listing 65-1 at the line marked 3) and the subtraction operator (shown in Listing 65-1 at the line marked 4); notice that the addition operator has some overhead: a void constructor and a destructor call. The subtraction call, on the other hand, has no such constructor call. The only construction is the object in the main function that is being created to hold the result. How does this happen? The answer is, the compiler is optimized to understand that a returned object that is assigned can be created in the actual space that was allocated. Because the compiler can optimize an object that is constructed in a return statement, and just
“copy” the memory into the object the result is assigned to, it will save you a lot of time and overhead if you use this optimization. Also notice that if you pass an object by value, rather than by reference — as we do in the func function — a copy is made of the object and the various constructors and destructors called. If you are going to pass an object into a function, always pass it by reference. If you do not plan to change the object within that function, pass a constant reference.
Passing Objects by Reference
One of the problems with C++ is that not all of it can be implemented within any single class. That means you either end up with stand-alone functions or other objects that must receive objects in their methods. For example, consider what happens when you pass a stream to a function to output data:
int print_my_data(ostream& out)
{
out << “Dump of my data” << endl // Code to dump the data
}
In this simple example, we are passing a stream object to a function. Note that the stream is passed by reference, using the ampersand, rather than by value, which would make a copy of the object. Why do we do it this way? Well, suppose you tried to write the code another way, such as this:
int print_my_data( ostream out )
{
cout << “Dump of my data” << endl;
}
int main()
{
print_my_data( cout );
}
Passing Objects by Reference 411
You would get a compile error, because the ostream class copy constructor is private. Creating a private constructor is one way to avoid having copies made of your objects, but an easier one is to pass the object by reference. This avoids the overhead of a temporary object, avoids the call to the copy constructor and destructor, and avoids the problems with having objects that are modified in the function. Let’s look at an example of what I am talking about here.
1. In the code editor of your choice, create a new file to hold the code for the implementation of the source file.
In this example, the file is named ch65a.cpp, although you can use whatever you choose.
2. Type the code from Listing 65-2 into your file.
Or better yet, copy the code from the source file on this book’s companion Web site.
LISTING 65-2: PASSING BY REFERENCE
#include <iostream> #include <stdlib.h> #include <time.h>
using namespace std;
class Integer
{
int _iVal; public:
Integer()
{
cout << “Void constructor” << endl; _iVal = 0;
}
Integer( int iVal )
{
cout << “Normal constructor” << endl;
_iVal = iVal;
}
Integer( const Integer& aCopy )
{
cout << “Copy constructor” << endl; _iVal = aCopy._iVal;
}
~Integer()
{
cout << “Destructor” << endl;
}
int getInt() const
{
return _iVal;
}
void setInt(int iVal)
{
_iVal = iVal;
}
}; |
|
|
void func1( Integer i1 ) |
|
5 |
{ |
|
|
i1.setInt( 12 ); |
|
|
} |
|
6 |
void func2( Integer& i2 ) |
|
|
{ |
|
i2.setInt(12);
}
int main()
{
Integer i;
func1(i);
cout << “After func1, value = “ << i.getInt() << endl;
func2(i);
cout << “After func2, value = “ << i.getInt() << endl;
}
The two functions in this listing both attempt to change the value of the integer value stored in an object passed to them. In the first case (shown at 5), the object is passed by value (the entire
object is copied before sending it to the function). In the second case (shown at 6), the object is passed by reference (the address of the object is passed to the function). You would normally expect the two functions to have the same result because they do the same thing. As we will see when we run the program, however, the two have very different ending results.
3. Save the source file in the code editor and then close the editor application.
412 Technique 65: Optimizing Your Code
4. Compile the source code file with your favorite compiler on your favorite operating system.
5. Run the program on the console window of your favorite operating system.
If you have done everything correctly, you should see the following output on the console window:
$ ./a.exe |
|
|
Void constructor |
|
|
Copy constructor |
|
|
Destructor |
|
|
After func1, value = 0 |
|
7 |
After func2, value = 12 |
8 |
|
Destructor |
|
|
This code shows that passing a value by reference avoids the overhead of creating a temporary object. More importantly, this approach avoids the problem of making changes that aren’t reflected in the original object. Notice that the func1 function does not change the value of the integer variable (shown at 7 in the output). This is because the function accepts its argument by value, which makes a copy of the original object and modifies that copy, rather than the object itself. The func2 function, shown at 8 in the output, passes its object by reference, which means that the original object is modified, and the result is reflected in the calling routine.
Postponing Variable
Declarations
If you have ever programmed in a language other than C++, you are probably already used to the process of defining a variable before you use it. This was the way to program in C, FORTRAN, and BASIC, so most programmers kept that approach when they moved to the object-oriented C++ language. Doing so is a mistake, however, because it creates potentially unnecessary overhead in the code.
For example, consider the following function:
int func( char *ptr )
{
char *newPtr = new char[200]; if ( ptr == NULL )
{
delete newPtr; return –1;
}
//Do something with the newPtr variable
return 0;
}
The code shows no reason to initialize or define the newPtr variable before we check the input. If this was some class variable that was too large to instantiate efficiently, you’d be wasting a lot of time and memory by defining this variable without knowing whether you’d be using it.
In general, here are the steps you should always follow to optimize the instantiation of variables in a function or method:
1. First, do any input data validation.
If the data requires that you exit the function, do so before you create any variables locally.
2. Pass the data to existing classes as appropriate.
If the data you are using must be passed into a constructor or other method of a class, break the constructor for that class into two parts, validation and initialization.
For example, consider the following code for a class that tries to open a file:
int open_the_file(const char *strName)
{
//See whether the input character string is valid.
if (strName==NULL || strName[0]==0) return –1;