...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
Imagine that you want to make many modifications to data members of the World
class in the World::addPerson
function. You start with adding a new Person
object to a vector of persons:
void World::addPerson(Person const& person) { bool commit = false; m_persons.push_back(person); // (1) direct action
Some operation down the road may throw an exception and all changes to involved objects should be rolled back. This all-or-nothing semantic is also known as strong guarantee.
In particular, last added person must be deleted from m_persons
when the function throws. All you need is to define a delayed action (release
of a resource) right after the direct action (resource acquisition):
void World::addPerson(Person const& aPerson) { bool commit = false; m_persons.push_back(aPerson); // (1) direct action BOOST_SCOPE_EXIT( (&commit)(&m_persons) ) { if(!commit) m_persons.pop_back(); // (2) rollback action } BOOST_SCOPE_EXIT_END // ... // (3) other operations commit = true; // (4) turn all rollback actions into no-op }
The block below point (1)
is a ScopeExit
declaration. Unlike point (1)
, an execution of the ScopeExit
body will be delayed until the end of the current scope. In this case it will
be executed either after point (4)
or on any
exception.
The ScopeExit declaration starts with BOOST_SCOPE_EXIT
macro invocation which accepts
Boost.Preprocessor sequence
of captured variables. If a capture starts with the ampersand sign &
, a reference to the captured variable
will be available inside the ScopeExit body;
otherwise, a copy of the variable will be made after the point (1)
and only the copy will be available inside the body.
In the example above, variables commit
and m_persons
are passed by
reference because the final value of the commit
variable should be used to determine whether to execute rollback action or
not and the action should modify the m_persons
object, not its copy. This is a most common case but passing a variable by
value is sometimes useful as well.
Consider a more complex case where World::addPerson
can save intermediate states at some points and roll back to the last saved
state. You can use Person::m_evolution
to store a version of changes
and increment it to cancel all rollback actions associated with those changes.
If you pass a current value of m_evolution
stored in the checkpoint
variable
by value, it will remain unchanged until the end of aa scope and you can compare
it with the final value of the m_evolution
.
If the latter wasn't incremented since you saved it, the rollback action inside
the block should be executed:
void World::addPerson(Person const& aPerson) { m_persons.push_back(aPerson); // This block must be no-throw Person& person = m_persons.back(); Person::evolution_t checkpoint = person.m_evolution; BOOST_SCOPE_EXIT( (checkpoint)(&person)(&m_persons) ) { if(checkpoint == person.m_evolution) m_persons.pop_back(); } BOOST_SCOPE_EXIT_END // ... checkpoint = ++person.m_evolution; // Assign new id to the person World::id_t const prev_id = person.m_id; person.m_id = m_next_id++; BOOST_SCOPE_EXIT( (checkpoint)(&person)(&m_next_id)(prev_id) ) { if(checkpoint == person.m_evolution) { m_next_id = person.m_id; person.m_id = prev_id; } } BOOST_SCOPE_EXIT_END // ... checkpoint = ++person.m_evolution; }
Full code listing can be found in world.cpp.