Home > Articles > Programming > C/C++

C++ Reference Guide

Hosted by

Toggle Open Guide Table of ContentsGuide Contents

Close Table of ContentsGuide Contents

Close Table of Contents

Lazy Evaluation

Last updated Jan 1, 2003.

Back in my school days, I developed a technique for doing my homework assignments only when I had to -- the day before I had to turn them in. Sometimes, I would even do my assignments on the very day of submission! What is considered a highly-reproached attitude in school proves to be a useful technique in software design, called "lazy evaluation." Lazy evaluation means deferring a certain operation (object initialization, a function call, allocating a resource, etc.) until it’s truly needed.

Rationale

Lazy evaluation has a few manifestations in software engineering. Copy-on-write, reference-counting, and singleton all rely on the deference of a time- or resource-consuming operation until there’s no escape from it. The simplest form of lazy evaluation consists of localizing the declarations of variables. Other forms include late binding of a pointer to an object, and accessing an object via an intermediary function.

The main advantage of lazy evaluation is that you avoid unnecessary overhead when possible. Often, the decision whether an object is necessary can only be done at runtime. If, for example, an application allows users to change the default language, there’s no point in loading in loading the foreign language strings before the user has actually selected a different language (as do some poorly designed applications, without naming names). Similarly, if a user opens a text document without modifying it, there’s no point in saving the unmodified document every ten minutes.

However, performance isn’t the only reason for adopting lazy evaluation. In some applications, it can simplify the program’s structure by localizing the conditional operation to the relevant code section. For example, a media player doesn’t need to load all of its codecs at startup. It’s better to load them on demand, according the media file that the player is currently playing. This way, the program is easier to maintain and debug.

Implementation

Let’s see some applications of this technique.

One classic example is the declaration of loop variables. In K"R C and C89, you are forced to declare i before the IF statement:

//C89 or poor style C++
int i=0;
if (some_condition)
 for (; i< MAX; i++) /assuming that no one has tampered with i
 {
 //..do something
 }
else
//no loop here

C++, in a stark deviation from ANSI C, permits declarations of local variables almost anywhere in a block. C99 adopted this feature from C++. Both C++ and C99 allow you to rewrite the previous code listing as follows:

if (some_condition)
 for (int i=0; i< MAX; i++)
 { //i is local to the for-loop
 }
else
//no loop here

The loop executes only when a certain condition is met. Therefore, it makes sense to declare i only in the scope of that loop. Deferring the declaration of i has two advantages:

  • Name localization. The scope of this variable is restricted to the loop’s body, so it doesn’t clash with other identifiers in the enclosing scope; nor is it possible to tamper with it outside the loop.
  • Performance. No memory is allocated for this variable and its initialization is elided if the condition is false.

Admittedly, for a built-in type, this overhead is negligible. However, replace i with a string object -- or better yet, a matrix object -- and witness the performance impact!

Late Initialization

In more realistic cases, you need to defer the construction of an object. Yet unlike with local variables, a definition of an object also entails its initialization so you can’t defer it. To overcome this restriction, you should either move the definition to an inner scope in which that object is unconditionally needed, or use some form of indirection.

Pointers are the most common form of indirection. Instead of defining a value object, define a pointer initialized to 0. Only when the object in question is needed do you allocate it dynamically and bind it to the pointer:

string * p=0;
if (string_needed)
{
 if (!p) p=new string;
//.. use the string via p
}
 return p;

This style of programming eliminates the overhead of the string initialization at the expense of complicating the code. It also introduces new security risks such as forgetting to delete the string before the program terminates. For this reason, you want to use smart pointers instead of bare pointers:

std::tr1:shared_ptr<std::string> p; 
if (string_needed)
{
 if (!p) p=new std::string;
//.. use the string via p
}
 return p;

If this usage pattern looks familiar, it’s no coincidence. The singleton pattern is based on similar principles, except that it wraps the conditionally-created resource in a function call.

Pay as You Go

Many years of C and Pascal programming taught us to declare everything in advance, in exactly one place. This practice had several pedagogical benefits such as forcing programmers to do some sort of design before actually writing the code, but modern programming languages have to cope with different challenges -- those that 1970s programming languages didn’t have to deal with. Often, deferring the declaration (or at least the initialization) of an object can improve your code’s performance, modularity, and readability.