...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
This is an example of using a badly designed File
class. An instance of File
doesn't close a file in a destructor, a programmer is expected to call the
close
member function explicitly.
File passwd; try { passwd.open("/etc/passwd"); // ... passwd.close(); } catch(...) { log("could not get user info"); if(passwd.is_open()) passwd.close(); throw; }
Note the following:
passwd
object is defined
outside of the try
block because
this object is required inside the catch
block to close the file,
passwd
object is not
fully constructed until after the open
member function returns, and
passwd.close()
should not be called, hence the call to
passwd.is_open()
.
ScopeExit doesn't have any of these problems:
try { File passwd("/etc/passwd"); BOOST_SCOPE_EXIT( (&passwd) ) { passwd.close(); } BOOST_SCOPE_EXIT_END // ... } catch(...) { log("could not get user info"); throw; }
RAII
is absolutely perfect for the File
class introduced above. Use of a properly designed File
class would look like:
try { File passwd("/etc/passwd"); // ... } catch(...) { log("could not get user info"); throw; }
However, using RAII to build up a strong guarantee could introduce a lot of non-reusable RAII types. For example:
m_persons.push_back(person); pop_back_if_not_commit pop_back_if_not_commit_guard(commit, m_persons);
The pop_back_if_not_commit
class is either defined out of the scope or as a local class:
class pop_back_if_not_commit { bool m_commit; std::vector<Person>& m_vec; // ... ~pop_back_if_not_commit() { if(!m_commit) m_vec.pop_back(); } };
In some cases strong guarantee can be accomplished with standard utilities:
std::auto_ptr<Person> spSuperMan(new Superman); m_persons.push_back(spSuperMan.get()); spSuperMan.release(); // m_persons successfully took ownership.
or with specialized containers such as Boost Pointer Container Library or Boost Multi-Index Containers Library.
Imagine that you add a new currency rate:
bool commit = false; std::string currency("EUR"); double rate = 1.3326; std::map<std::string, double> rates; bool currency_rate_inserted = rates.insert(std::make_pair(currency, rate)).second;
and then continue a transaction. If it cannot be completed, you erase the currency
from rates
. This is how you
can do this with ScopeGuard
and Boost.Lambda:
using namespace boost::lambda; ON_BLOCK_EXIT( if_(currency_rate_inserted && !_1) [ bind( static_cast< std::map<std::string,double>::size_type (std::map<std::string,double>::*)(std::string const&) >(&std::map<std::string,double>::erase) , &rates , currency ) ] , boost::cref(commit) ); // ... commit = true;
Note that
if_
expression
refers to commit
variable
indirectly through the _1
placeholder,
if_[ ... ]
requires in-depth knowledge of Boost.Lambda
and debugging techniques.
This code will look much better with native lambda expressions proposed for C++0x:
ON_BLOCK_EXIT( [currency_rate_inserted, &commit, &rates, ¤cy]() -> void { if(currency_rate_inserted && !commit) rates.erase(currency); } );
With ScopeExit you can simply do
BOOST_SCOPE_EXIT( (currency_rate_inserted)(&commit)(&rates)(¤cy) ) { if(currency_rate_inserted && !commit) rates.erase(currency); } BOOST_SCOPE_EXIT_END // ... commit = true;
In future releases ScopeExit will take advantages of C++0x features.
BOOST_SCOPE_EXIT(currency_rate_inserted, &commit, &rates, ¤cy) { if(currency_rate_inserted && !commit) rates.erase(currency); } BOOST_SCOPE_EXIT_END
BOOST_SCOPE_EXIT_END
will
be replaced with a semicolon:
BOOST_SCOPE_EXIT(currency_rate_inserted, &commit, &rates, ¤cy) { if(currency_rate_inserted && !commit) rates.erase(currency); };
&
and =
:
BOOST_SCOPE_EXIT(&, currency_rate_inserted) { if(currency_rate_inserted && !commit) rates.erase(currency); };
this
pointer.
ScopeExit is similar to scope(exit) feature built into the D programming language.
A curious reader may notice that the library doesn't implement scope(success)
and scope(failure)
of the D
language. Unfortunately, it's not possible in C++ because failure or success
condition cannot be determined by calling std::uncaught_exception
.
It's not a big problem, though. These two constructs can be expressed in terms
of scope(exit)
and a bool commit
variable as explained in Tutorial.
Refer to Guru of the Week #47
for more details about std::uncaught_exception
and if it has any good use at all.