Yes and no. This turned into a religious debate on another forum elsewhere. Some say: "With multiple returns, it's hard to tell what executes and what doesn't, and can lead to bugs with cleanup code not running due to an early return that should have run." I say: "Functions should be small, first off. Secondly, it's still hard to tell what code executes and what doesn't because you have to cause execution to 'snake' its way through to the bottom of the function. Thirdly, proper use of RAII and resource manager objects eliminates the cleanup code problem automatically, and such techniques are required anyway, if the code is to be exception safe."
Examples:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
struct Person {
std::string firstName;
std::string lastName;
};
// I prefer this, with an "early return"
bool IsNameFound( const std::deque<Person>& people, const std::string& first, const std::string& last ) {
typedef std::deque<Person>::const_iterator Iter;
for( Iter i = people.begin(); i != people.end(); ++i )
if( boost::bind( &Person::firstName, _1 ) == first && boost::bind( &Person::lastName, _1 ) == last )
return true;
return false;
}
// Others prefer this
bool IsNameFound( const std::deque<Person>& people, const std::string& first, const std::string& last ) {
typedef std::deque<Person>::const_iterator Iter;
bool found = false;
for( Iter i = people.begin(); i != people.end() && !found; ++i )
if( boost::bind( &Person::firstName, _1 ) == first && boost::bind( &Person::lastName, _1 ) == last )
found = true;
return found;
}
|
Personally, I don't find the second as understandable as the first because the second requires me to understand how the "found" variable works. Now in this case, it really isn't any harder -- it's kinda obvious -- but in more complex examples it can be harder. Consider a slightly more complex example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
typedef std::vector< std::vector< std::string > > String2DVec;
// My preferred way, with an 'early return':
bool ReplaceString( String2DVec& vec, const std::string& oldStr, const std::string& newStr ) {
typedef String2DVec::value_type::iterator VIter;
for( String2DVec::iterator i = vec.begin(); i != vec.end(); ++vec )
for( VIter j = i->begin(); j != i->end(); ++j )
if( *j == oldStr )
{
*j = newStr;
return true;
}
return false;
}
// Others prefer:
bool ReplaceString( String2DVec& vec, const std::string& oldStr, const std::string& newStr ) {
typedef String2DVec::value_type::iterator VIter;
bool found = false;
for( String2DVec::iterator i = vec.begin(); i != vec.end() && !found; ++vec )
for( VIter j = i->begin(); j != i->end(); ++j )
if( *j == oldStr )
{
*j = newStr;
found = true;
break;
}
return found;
}
|
The statement "return found;" is very well self-documenting, but the logic to get there in my opinion is a little roundabout. And still others would write it even more convoluted (imo):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
bool ReplaceString( String2DVec& vec, const std::string& oldStr, const std::string& newStr ) {
typedef String2DVec::value_type::iterator VIter;
bool found = false;
for( String2DVec::iterator i = vec.begin(); i != vec.end(); ++vec )
{
for( VIter j = i->begin(); j != i->end(); ++j )
if( *j == oldStr )
{
*j = newStr;
found = true;
break;
}
if( found )
break;
}
return found;
}
|
EDIT: and then there are still other purists who say "break is nothing more than a goto" (no goto wars here, please):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
bool ReplaceString( String2DVec& vec, const std::string& oldStr, const std::string& newStr ) {
typedef String2DVec::value_type::iterator VIter;
bool found = false;
for( String2DVec::iterator i = vec.begin(); i != vec.end() && !found; ++vec )
{
for( VIter j = i->begin(); j != i->end() && !found; ++j )
if( *j == oldStr )
{
*j = newStr;
found = true;
}
}
return found;
}
|
I prefer concise implementations to verbose ones.