This topic has come up before.
The general rule is to minimize scope of declarations except where performance is adversely affected. The problem is that most people don't understand when that is. Take this example:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
for( size_t file = 0; file < numFilesInDir; ++file ) {
std::vector< std::string > lines;
std::ifstream input_file( "file_" + boost::lexical_cast<std::string>( file ) + ".txt" );
while( input_file ) {
std::string text_line;
std::getline( input_file, text_line );
if( input_file )
lines.push_back( text_line );
}
std::for_each( lines.begin(), lines.end(), std::cout << "Contents of file:\n" << boost::lambda::_1 << '\n' );
}
|
Now, let's make a concrete example. Say there are 2 files in the directory, both containing 5 lines of text.
What does the above program do, with respect to the std::vector<std::string> and the std::string inside
the while loop?
First, let's just look at the while loop, which will run 6 times for a 5-line file (the last time it will not do push_back).
Each of those six times, it first default constructs the std::string, then assigns it a value (std::getline) and then
destroys it. What is default construction? Well, construction in general means setting up the vtable for the object
and calling the constructor. Since std::string() has no virtual functions, there is no vtable to set up, thus that part
is a no-op. The constructor is going to initialize the string to an empty string, which means it will not allocate any
memory.
Next, for assigning it a value. Since the string is already empty, there is no memory to deallocate, so .clear() will
do nothing [if std::getline() calls it to empty the string] and operator=() will just allocate memory and copy the
source string into the object.
Last, for destroying it. Destruction means calling the destructor, which in our case means freeing the memory
allocated to the object, and then destroying the vtable, of which there is none.
Oh, and one more thing. Technically speaking a stack variable that is created would cause the compiler to allocate
stack space (usually just incrementing/decrementing a single CPU register) and one that is destroyed would cause
the compiler to free the stack space allocated to it (a decrement/increment of a single CPU register), however any
reasonable compiler will optimize both of those operations out of the while loop altogether.
So, in summary, the above code actually does this:
1. Allocate stack space exactly once [ before entering the while loop ]
2. Set all internal data members of std::string (default construct) 6 times [ default construction ]
3. Allocate memory and assign a string 5 times [ std::getline ]
4. Deallocate memory 5 times [ destruction ]
5. Deallocate stack space exactly once [ after exiting the while loop ]
Now, let's "optimize" the loop by moving the declaration of text_line outside the while loop:
1 2 3 4 5 6
|
std::string text_line;
while( input_file ) {
std::getline( input_file, text_line );
if( input_file )
lines.push_back( text_line );
}
|
We default construct text_line once, obviously. The first time through the loop, text_line has to be empty, so
the first call to std::getline() just has to allocate memory and copy the target into it. The next four times through
the loop, std::getline() will actually read the remaining lines from the file, and thus it needs first to deallocate the
memory held by the string from the previous iteration and then reallocate memory to store the new one. The
final time through the loop, std::getline() will deallocate the memory held by the string but won't read anything,
so no memory will be allocated. Then we destroy the string, which will be empty.
In summary:
1. Allocate stack space exactly once [ before entering the while loop ]
2. Set all internal data members of std::string (default construct) 1 times [ default construction ]
3. Deallocate memory and set all internal data members of std::string 5 times [ first half of std::getline ]
3. Allocate memory and assign a string 5 times [ second half std::getline ]
4. Call destructor, which will do nothing since string was already empty [ destruction ]
5. Deallocate stack space exactly once [ after exiting the while loop ]
Let's compare results:
Both allocate and deallocate stack space once.
Both perform 5 memory allocations.
Both perform 5 memory deallocations.
In other words, they are effectively the same from a performance standpoint. You have to think about it.
operator=() for a type is almost always effectively the same thing as running a destructor followed by a
copy constructor, the only difference being that vtables are not set up and destroyed. However, two things
about that. First, none of the STL containers have vtables, since none have virtual methods, and second,
the relative CPU time spent performing vtable setup/destruction is insignificant compared to even a single
memory allocation.
But, you say, what about the std::vector<std::string> in the for loop()? Surely it's expensive to construct
and destroy it, right? Answer: no more than if you were to move the variable outside the for() loop, because
if you did that, you'd have to call clear() on the vector after each iteration. And what does clear do()? Well,
deallocates all the memory, just as destructor would, just as operator= would. When there is no vtable,
it is no more expensive running the destructor and constructor than it is to call clear().
** Lastly -- for the astute reader, I am aware of std::vector<>::reserve(), however I deliberately avoided
mentioning it here to keep the example simple. For the purposes of this discussion, I assume that only
a single memory allocation is needed, whereas in fact with std::vector<>'s implementation that is not the
case. Not only is that a simple fix here -- reserve( 5 ) or whatever -- but it affects both approaches equally.