Atomic is a C++ container library that provides transactions. Atomic containers can be rolled back to any point in their past. The purpose of this is to make it easy and efficient for programmers to implement strongly exception-safe code. Instead of analysing a program for exceptions in minute detail, a programmer can simply declare a transaction, and all operations within that transaction are atomic.
void f()
{
atomic::transaction tr;
g();
h();
i();
tr.commit();
}
If the function f() encounters and exception, the transaction will roll back all of the changes in its scope, including all changes in g(), h() and i() if they use atomic containers. Similarly if f() is used as part of a larger transaction, then it can be rolled back.
#include <atomic/transaction.hpp>
A transaction is a unit of work that either succeeds completely or rolls back all of its changes. A transaction object is declared on the stack, and the scope of the object defines the scope of the transaction. The destructor of a transaction will roll back all operations within its scope (including operations performed in called functions), unless the commit() function has been called. commit() sets a flag so that the transaction does not roll back. For example
{
atomic::transaction tr;
animals.push_back("cat");
animals.push_back("dog");
tr.commit();
}
If the second push_back() fails, animals is rolled back to as it was at the start of the transaction. Rolling back is guaranteed to succeed and not throw an exception, even when it needs to insert deleted items back into a container.
There is also atomic::auto_transaction, which automatically commits the transaction, but rolls back the transaction when an exception occurs.
{
atomic::auto_transaction tr;
animals.push_back("cat");
animals.push_back("dog");
}
Transactions can be nested, for example if one function with a transaction calls another function with a transaction. Transactions should not be declared anywhere other than as stack variables, because this guarantees the correct nesting.
For example:
f()
{
atomic::transaction t1;
...
g();
...
h();
...
t1.commit();
}
g()
{
atomic::transaction t2;
...
t2.commit();
}
h()
{
atomic::transaction t3;
...
t3.commit();
}
The function g() is strongly exception safe because if the function g() throws an exception, then the destructor of t2 rolls back any operations performed in g(). The function h() is strongly exception safe for the same reason. The function f() is strongly exception safe because if either of f(), g() or h() throw an exception, then the destructor of t1 will roll back all operations performed in f(), g() and h().
g() can still be rolled back, even after the call to t2.commit(). If f() encounters an exception then the destructor of t1 will roll back everything including g().
The atomic containers are
In addition there are atomic pointers and values
All of these class templates can participate in transactions, and can be rolled back.
These containers are very similar to their corresponding Standard Library containers, but have some differences:
#include <atomic/value.hpp>
atomic::value manages a single value so that it can be rolled back to any point in its history. For example
atomic::value<std::string> name = "bill";
{
atomic::transaction t1;
name = "tom";
assert(*name == "tom");
{
atomic::transaction t2;
name = "fred";
assert(*name == "fred");
t2.commit();
}
assert(*name == "fred"); // Committed by t2
}
assert(*name == "bill"); // Rolled back by t1
The stored value is const, and can only be modified via assignment. The value can be accessed via conversion, operator*() or operator->().
#include <atomic/shared_ptr.hpp>
This class template is similar to std::tr1::shared_ptr (as described in TR1). However there are functions missing, in particular the capability to cast in between pointer types. This is a limitation of the current implementation.
If the full features of std::tr1::shared_ptr are needed, then atomic::value<std::tr1::shared_ptr> should be used, as this will give much better compatibility.
The implementation of this class keeps references to the pointee on the transaction stack. These extra references don't affect the use_count(). The pointee will be kept alive for the duration of the transaction (in case the transaction needs to be rolled back).
For example:
atomic::shared_ptr<int> p(new int(1));
atomic::weak_ptr<int> q(p);
{
atomic::transaction tr;
p.reset(new int(2));
assert(q.expired());
assert(*p == 2);
}
assert(*p == 1); // Rolled back by tr
assert(!q.expired()); // Rolled back by tr
#include <atomic/weak_ptr.hpp>
This class template is the equivalent to std::tr1::weak_ptr (as described in TR1). It lacks the ability to convert between different pointer types.
#include <atomic/list.hpp>
atomic::list is the atomic version of std::list. All of the functions behave the same as std::list, except that the functions swap(), merge(), sort(), splice() and reverse() are not implemented.
atomic::list behaves slightly differently depending upon whether the object it is containing is atomic.
atomic::list<int> list1;
atomic::list< atomic::value<int> > list2;
In list1, the items in the list cannot be modified. The iterators are const, and front() and back() return const references to the item. In list2, the items may be modified. The iterators are non-const, and front() and back() can return non-const references to the item.
pop_back() and pop_front() are different, since they return a reference to the object that was popped from the list! In a std::list this would not be possible since the reference would be to a deleted object. For example
atomic::list<Item> list1, list2;
void process1()
{
atomic::transaction tr;
while(!list1.empty())
{
list2.push_back(list1.front());
list1.pop_front();
}
tr.commit();
}
void process2()
{
while(!list1.empty())
{
atomic::transaction tr;
list2.push_back(list1.front());
list1.pop_front();
tr.commit();
}
}
In the function process1(), if an exception is encountered, then the function exits as if no items had been moved from list1 to list2. In function process2(), if an exception is encountered, then the function exists with some of the items moved from list1 to list2. It is still necessary to use a transaction so that items are not lost if pushing onto list2 fails. process2() can still be rolled back in its entirety if it is part of a larger transaction.
#include <atomic/vector.hpp>
atomic::vector is the atomic equivalent of std::vector. The main difference is that the only operation is push_back(). The operations push_front(), pop_front(), pop_back(), insert(), erase(), assign() and swap() are not implemented because they cannot be rolled back safely (at least not efficiently).
The other difference is that the items and iterators are const, unless the container contains an atomic::value. This is so that the contents of the vector cannot be modified in a non-atomic way.
The methods
template<typename Iterator>
void push_back(Iterator from, Iterator until);
void push_back(size_type n, const value_type &t);
insert a number of items onto the back of the list.
Example:
atomic::vector<char> log;
void log_message(const std::string &message)
{
log.push_back(message.begin(), message.end());
}
The function log_message() is atomic because push_back() is atomic.
#include <atomic/map.hpp>
These containers are the atomic versions of std::map and std::multimap. Updating atomic::map must be done differently because the [] operator returns a const reference.
One cannot write
atomic::map<std::string, int> m1;
m1["dog"] = 12;
because the [] operator returns a const reference. Instead, one must insert the item using the insert() method:
m1.insert(std::make_pair("dog",12));
On the other hand, if one wants to replace an item, one must call the replace() method, which behaves like insert, but will replace the stored value.
m1.replace(std::make_pair("dog",13));
atomic::map follows the same rules regarding constness. Items in the map are const unless they are atomic. Thus
atomic::map<std::string, atomic::value<int> > m1;
m1["dog"] = 12;
works fine, but be aware that it is less efficient to use atomic::value.
#include <atomic/set.hpp>
These containers are the atomic versions of std::set and std::multiset. As with the other containers, the iterators are const.