template<class T>
class regina::Snapshot< T >
Keeps a snapshot of an object of type T as it was at a particular moment in time.
To describe how this class works, we need some terminology:
- the image is a single object of type T whose snapshot we are taking;
- the viewers are many object of other types that all require access to this snapshot.
The life cycle of this process is as follows:
- An image I is created and modified over time; initially it remains uninvolved in the snapshotting machinery.
- At some point in time, a viewer V1 wishes to take a snapshot of I. To do this, it creates a snapshot reference
SnapshotRef(I)
. This is a cheap operation that "enrols" I in the snapshotting machinery, by creating a single Snapshot object S.
- More viewers may take snapshots of I, either by creating a new
SnapshotRef(I)
or by copying other viewers' snapshot references. Again, these are all cheap operations. All references to I will refer to the same snapshot object S.
- If the image is about to modify itself or be destroyed, it notifies its snapshot S, which in turn takes a deep copy of I and stores it for safekeeping. This is an expensive operation. The original image now loses its link to S, and becomes "unenrolled" from the snapshotting machinery again; the only way to access the original snapshot at this point is by copying other references.
- After the image was modified, making a new
SnapshotRef(I)
will re-enrol I and create a completely new Snapshot object. The original Snapshot may of course still exist, maintaining its copy of I as it used to be.
- Each snapshot S is reference counted: when the last reference to it is destroyed, then S is also destroyed (along with the deep copy of the original image, if one was ever made).
Regarding access to the image:
- A Snapshot and SnapshotRef can outlive the original image I.
- It is important that every snapshot reference only ever accesses the underlying image via the SnapshotRef dereference operators. This is because the image may change to be a different object if the original is modified or destroyed.
- Snapshot references are only ever granted read-only access to the image. Semantically this makes sense (since this is a snapshot); also this avoids unpleasant lifespan questions if someone tries to modify a Snapshot's internal deep copy. If you need write access from a snapshot, then take your own deep copy and modify that: it is no less efficient (since the snapshot would have taken a copy otherwise), and this way the ownership and lifespan semantics are clear. Any attempt to cast away constness and modify the image through a snapshot may result in a regina::SnapshotWriteError exception being thrown.
This snapshotting machinery is, in some ways, a slimmed-down variant of copy-on-write machinery, designed to be easily plugged into type T. The requirement on T are as follows:
- T must derive from the base class Snapshottable<T>. The overhead is one extra pointer to the storage for T.
- If T supports move/copy construction, move/copy assignment and/or swapping, then their implementations must call the corresponding operations from the base class Snapshottable<T>.
- In particular, T must have a copy constructor. This will be used by the snapshot whenever it needs to take its own deep copy.
- Whenever an object of type T changes, it must call Snapshottable<T>::takeSnapshot() from within the modifying member function, before the change takes place (though there are a handful of exceptions to this requirement, described in the Snapshottable class notes). If the object does not have a current snapshot, this is very fast (a single test for a null pointer). If the object does have a current snapshot, then this will be expensive since it will trigger a deep copy.
- Likewise, in the destructor for T, the first call should be to Snapshottable<T>::takeSnapshot().
- If you are doing a complex series of modifications, it is not essential to worry about multiple calls to takeSnapshot() coming from nested modificiation routines, since only the first of these calls could be expensive. (This is in contrast, for instance, to the packet listener machinery, where nested modifications need to be managed more carefully because firing a packetToBeChanged() event will always iterate through all registered listeners.)
Regarding multithreading:
- In general, this class is not thread-safe; in particular, the code that creates new snapshots, takes deep copies before modification, destroys snapshots, and enrols/unenrols images from the snapshot machinery is all unsafe for multithreading.
- However: the reference counting machinery is thread-safe. This means that, if your image is not being modified and you already have one snapshot reference R, it is safe to create more references, access the image through your references, and/or destroy references, all from multiple threads, as long as one reference always remains.
- This means that, again assuming your image is not being modified, you can hold on to a particular snapshot reference R and use that as a "temporary guarantee" of thread-safety.
- All of this requires a bit of care. However, the trade-off is that frequent operations (including Snapshottable<T>::takeSnapshot() which must be called on every object of type T before every modification, even if no snapshots exist), can take place without the overhead of locking and unlocking mutexes.
This Snapshot class itself should remain forever behind the scenes: end users cannot access it and should not know about it. Images should always work through their base class Snapshottable<T>, and viewers shoudl always work through SnapshotRef<T>.
- Python
- Not present.