The DataReader API must provide an exception-safe way to retrieve samples and must specify guarantees when exceptions are thrown. The exception safety of the DataReader API is analyzed with respect to user level state and middleware state. Read and take are both logically non-const operations with respect to the m/w because both of them have side effects such as changes in the sample_state and instance_state. There could be exceptions while delivering samples from m/w space to user space on the boundary of the read/take function calls. Depending upon the implementation there may or may not be a way to roll-back the changes in sample_state and instance_state. However, it is critical to have a way to not lose the samples even in case of exceptions. In general, it is difficult to provide strong guarantees with respect to m/w for datareader api.
1. Strong and no throw guarantees for LoanedSamples:
The constructor of LoanedSamples should provide strong guarantees. All the remaining operations (specifically copy-ctor and copy-assignment operator) should provide no-throw guarantees. It is important for LoanedSample to not throw exception during copy because then the samples would be lost. Further, read/take api should minimize dynamic memory allocation and locks on the critical path for performance reasons. Therefore LoanedSamples can’t use reference semantics either. (Reference<D> uses shared_ptr, which uses dynamic memory allocation and mutex to construct and protect the reference count.)
These strict guarantees for LoanedSamples appear to be essential to support at least minimally exception safe read/take api that does no lose samples in case of exceptions. It is also the intent of LoanedSamples<T> to return the resources to m/w as soon as it goes out of scope.
Proposed Solution: Use std::auto_ptr semantics in LoanedSamples. A key property of std::auto_ptr is that it can be safely returned while moving the resources out of an inner scope to an outer. No exceptions will be thrown. LoanedSamples<T> should be designed as such to avoid exceptions on copy. There is an idiomatic way of implementing such a class: The Move-constructor Idiom.
Consequences: No std::vector<LoanedSamples<T>> would be possible. However the anticipated use-case of LoandedSamples is that the users would iterate over the range of samples and copy them and return the loan. For those users who really want to create vectors of loaned buffers, they would take ownership of the buffer from the LoanedSamples and use std::vector<shared_ptr<T> > to automate memory management. Use of custom deleters feature of shared_ptr may facilitate returning of the loan.
2. LoanedSamples take(): Basic exception safe.
The take function is loaning a buffer from the m/w. The implementations may or may not provide a way to ‘untake' samples. So this function cannot provide strong exception safety with respect to m/w. Therefore, guarantees of LoanedSample above are applicable here. This would guarantee that the samples that are removed from the history cache are not destoyed before the user gets a chance to access them.
3. LoanedSamples read(): Basic exception safe.
An attempt can be made to reread the samples. Above guarantees of LoanedSamples are applicable here
4. // — Forward Iterators: — //
void read(SamplesFWIterator, InfoFWIterator, size_t max_samples): Basic exception safe void take(SamplesFWIterator, InfoFWIterator, size_t max_samples): Basic exception safe
This API probably needs to specify guarantees with respect to two different things:
1. With respect to state changes in the user supplied range
2. With respect to m/w state
Changes to user-supplied range ==== If Nth copy throws there is no way to recover earlier N-1 objects that were already copied successfully. So in general only basic guarantee is provided. If the first object in the range throws, nothing changes and strong safety is provided. Further the API must provide a way to return # of objects were actually copied. API should specify a precondition: iterators must be from a range that is initialized and contain valid max_samples objects. In other words, the range cannot contain any uninitialized object. This prohibits uses such as
dr.read(v.begin(), v.end(), max);
Changes to m/w state ==== The read() function has side effects like changes in the view_state and instance_state so it's really not a const function. The implementations may or may not provide a way to ‘unread' samples. So with respect to these status bits there is no way to provide strong exception guarantee so only basic guarantee is provided.
Proposed Solution: In case of an exception (e.g., std::bad_alloc) during these function calls, construct an internal LoanedSamples<T> object and wrap it in a ‘ReadTakeException’ object and throw it. ReadTakeException class must provide an API to retrieve the LoanedSamples. Nothrow guarantees of LoanedSamples are important here again. This solution provides at least one way to retrieve the samples that are read/take from the history cache. Such a wrapper may also need to wrap the original exception that was raised to support exception neutrality.
5. // — Back-Inserting Iterators: — //
read(SamplesBIIterator, InfoBIIterator, size_t max_samples): Basic exception safe take(SamplesBIIterator, InfoBIIterator, size_t max_samples): Basic exception safe
In addition to the exceptions that forward iterator versions can throw, these functions can throw when vector::push_back throws. I think the same rules are applicable to these functions.