Efficient C++ Programming
- Locality of Declaration
- Returning a Class Object
- Initialization versus Assignment of Class Objects
Given the following Point3D class (with a copy constructor, copy assignment operator, and destructor)
class Point3D { public: Point3D( float=0.0f, float=0.0f, float=0.0f ); Point3D( const Point3D& ); ~Point3D(); Point3D operator=( const Point3D& ); Point3D operator+( const Point3D&, const Point3D& ); // ... };
and the following two Point3D class objects
Point3D pt1( 0.0f, 2.0f, -5.0f ), pt2( 2.0f, 0.0f, -5.0f );
which is the more efficient: the initialization of new_origin with the return value of the overloaded addition operator:
Point3D new_origin = pt1 + pt2;
or its assignment:
new_origin = pt1 + pt2;
The initialization of new_origin is always more efficient than its assignment (we'll see why in the next section). In both cases, the final result is the same: new_origin holds the result of adding pt1 and pt2. The issue is one of performance, not correctness. The difference is in the amount of work it takes to get to the same result.
Is the difference in performance significant? That depends both on how often this particular form of initialization versus assignment occurs within the program, and the size and complexity of the class involved. In this case, the increased program efficiency comes largely for free after we recognize and internalize the assignment versus initialization idiom. One primary focus of this text is to identify efficient C++ programming idioms. Locality of declaration is as good a starting point as any.
Locality of Declaration
In the C language, within a block, all definitions must appear before any program statements. A C programmer, for example, writes
void foo() { // In C, all objects within a block // must be declared before the first // program statement int ival, ix; int *ptr; // ok: now begin programming ... }
even if the variables ival, ix, and ptr are not used until 50 or 100 lines later. In C++, an object declaration, such as
int ival;
is itself a program statement, and may be placed anywhere within the program text.
Locality of declaration was the motivation for this difference between the C++ and C languages: to allow an object to be declared near its use rather than at the top of its containing block. For example,
// ptr only visible within body of if-statement if ( int *ptr = retrieve_department_codes() ) { /* ... ok: process array of codes ... */ } // ix is only visible within body of for-loop for ( int ix = 0; ix < vec.size(); ++ix ) { /* ... iterate across the vector ... */ }
With regard to the built-in numeric data types (such as integer, double, char, and so on) or the compound data types such as arrays and pointers, locality of declaration is a matter of personal style. With non-trivial class objects, howeverthat is, class objects that have associated constructor(s) and a destructorlocality of declaration affects program efficiency.
For example, consider the following code fragment using the C language idiom of defining our objects at the beginning of the block. There is no harm in coding like this in C because the declaration of an object has no side effects. This is not true in C++, in which a default constructor and destructor are associated with the two Matrix objects m1 and m2.
// inefficient C idiom using C++ { Matrix m1, m2; if ( !arrayMat ) return 0; if ( cacheItem( arrayMat )) { // check cache; if match, return 1; } // ok, do the real work here }
Although this fragment executes correctly, it is inefficient whenever either of the conditional statements evaluates as true. This is because m1 and m2 must always be constructed and destructed even if the program returns prior to actually making use of the objects. For example, here is a likely compiler expansion of the code:
// Pseudo C++ Code : likely compiler expansion { Matrix m1, m2; // invoke the default constructors ... m1.Matrix::Matrix(); m2.Matrix::Matrix(); if ( !arrayMat ) { // ok: must destroy Matrix objects m1.Matrix::~Matrix(); m2.Matrix::~Matrix(); return 0; } if ( cacheItem( arrayMat )) { // again: must destroy Matrix objects m1.Matrix::~Matrix(); m2.Matrix::~Matrix(); return 1; } // ok, do the real work here }
A typical response to this example is to ask why a sufficiently smart compiler could not suppress the construction and destruction of the objects in cases in which they are not explicitly used.
In general, the standard says that a compiler is allowed to remove an object only if the compiler is certain that the object is never used and its constructor and destructor do not have any side effects. In this example, the objects are used, but not until after the two conditional if-statements. So it is not an issue of removing the objects, but rather of moving their construction to after evaluation of the two conditional statements.
Couldn't the compiler then just simply delay the construction of the objects until after the conditional statements? No. The reason is that the compiler cannot know what the default constructor is actually doing and therefore whether it is safe to delay its invocation.
For example, a C++ design idiom is for a constructor to acquire a resource, perhaps a lock on shared memory, which is then freed within the destructor. Although the class object is never used explicitly, the program depends on the constructor side effects. Moreover, the construction needs to be timely; delaying it could be as problematic as not invoking the constructor at all.
Okay, you might argue. But why can't the compiler recognize that and only delay the construction in cases in which it is safe, such as with our Matrix objects? On a purely mechanical level, the problem is that the Matrix constructor is almost certainly going to be stored in a separate file. Moreover, it is likely to have been compiled long before our code fragment and stored in a shared library.
In general, this level of decisionwhen the constructor should be invokedis best handled by the programmer. The idiom is that of locality of declaration:
// ok: use locality of declaration under C++ { if ( !arrayMat ) return 0; if ( cacheItem( arrayMat )) { // check cache; if match, return 1; } // ok: only constructed and destructed // if execution reaches here Matrix m1, m2; // ok, do the real work here }
A second reason to prefer locality of declaration when using class objects refers back to our original observation; that is, that the initialization of a class object, such as
Matrix m1, m2; // stuff using m1 and m2 Matrix composite = m1 + m2;
is more efficient than the semantically equivalent assignment
Matrix m1, m2, composite; // same stuff using m1 and m2 composite = m1 + m2;
A reasonable question at this point is to ask why? Let's see what sense we can make of this in the following two sections.