1. suppose I have a class called Cement whose instances are immutable when just read from the database or are just written to the database
2. all other instantiations in memory (generated by other classes) are mutable until written to the database
what do you think is the most expressive approach to enforce these constraints in C++ code with minimal comments and explanations?
sidenote: this kind of pattern is useful for real-life problems like DB time-travel (anything written to the DB becomes immutable and undeletable), or calculating Money values and recording them to the DB in accounting systems (any reported Money values become real, in contrast to calculated values which may have fractional pennies)
One thing that instantly jumped into my head ( for better or for worse ;P ) would be a reference tracking system like boost::weak_ptr has implemented. Essentially, the idea would be to wrap the Cement classes with a template class, and when it comes time to mutate that class, you would have to call the equivalent of boost::weak_ptr::lock() on it in order to effectively "lock in" a mutation. Boost::weak_ptr returns a null pointer if no references are still made to the shared object. Your implementation might return a null pointer if a state flag on the shared object is set.
#include <boost/smart_ptr.hpp>
template<typename T>
void mutate(boost::weak_ptr<T> weakPtr)
{
boost::shared_ptr<T> sharedPtr = weakPtr.lock();
if(sharedPtr != nullptr)
{
//do something
}
else
{
}
}
class Thingy { };
int main()
{
boost::weak_ptr<Thingy> weakPtr;
{
boost::shared_ptr<Thingy> thingyPtr(newint(4));
weakPtr = boost::weak_ptr<Thingy>(thingyPtr);
mutate(weakPtr); // something is done to the object pointed to
}
mutate(weakPtr); // Nothing is done to the object pointed to.
}
I can't compile this where I'm located right now, but you get the idea. If not, let me know.
Carefully watch the following code, it hasn't been compiled. Obviously left out reference tracking, debug assertions, synchronization issues, exception safety, etc.
ok, I've refactored the code - three things I don't like about my version though:
1. the throw on Line 28 (need a better way to tell me where to fix the code)
2. getMutable() is not as nice as an operator*(), but there seems to be no way to wedge it in
3. the canmutate_ bool variable is boiler-plate code that's going to be all over the place - in its current form, I will have to copy and paste all over the place - perhaps I need to change it to a superclass or a template CanMutate<>
1. the throw on Line 27 (need a better way to tell me where to fix the code)
2. getMutable() is not as nice as an operator*(), but there seems to be no way to wedge it in
I'm assuming this isn't just a temporary block while a background thread read/writes to a database, but a single thread where in memory objects are being written to a database for memory/speed requirements.
I don't really understand the use case for this. If I am in a piece of code which has a mutatable object that for whatever reason I want to mutate - what am I expected to do when I find that the object can not be mutated (ie when I catch the exception), and is not likely to change status if I wait? Can you give me a real example where throwing an exception and letting the calling code handle it is the best solution?
Surely the best thing is to implement copy on write, or some similar idea?
kev82, thanks for your thoughtful insight - copy on write didn't cross my mind when I was considering my requirements, but let me think a little bit - it certainly might be the most viable approach...
I have two places where I need this kind of functionality:
1. In a DB time-machine scenario when I keep track of all variants of a object (lets call each of these variants a record or a row in the DB) over time - this way, I can always query the database anywhere on this time-axis and be able to retrieve the state of the system any time in the past (hence, the time-travel name). In this kind of scenario, all variants are immutable - you may try to change the latest variant, but doing so should create a copy - a new mutable instance. (in this case, I think copy on write may work just fine)
2. In an accounting system, we have to use Decimal/Money arithmetic. One model is, we can use double or any high-precision float to do calculations, but once a figure shows up in a report (or is written to the database), we must convert back to a Decimal/Money object for real pennies (all pennies must add up!). This means when a Money amount is written to the DB, it is a fungible immutable quantity. We can do calculations with this amount using a mutable object, but once it's written out to the database, that instance becomes immutable.
1. and 2. are not mutually exclusive (2. can be implemented in terms of 1., in fact).
Let me see if I can rewrite the code with copy on write in mind (I will copy on write if the current instance is immutable - if it's already mutable, I don't mind) - ty!
kev82, how do you propose I implement copy on write? In every one of my setters, check to see if I need to make a copy before modifying? Seems kind of messy - I'm open to ideas, though...
I thought about your comment regarding the throw, though - I will replace it with a copy and return the mutable copy - that should work much better...
For situation 1, I would just keep them mutable all the time, but not have them encode a time component, keep that as a separate entity.
So your database store/retrieve functions would look like
commitState(std::pair<TimeOfState,State> p)
State RetrieveState(TimeOfState t)
The state objects are always mutable, which makes sense as they don't have an innate point of time.
If the State objects are large then it would be quicker (but not necessary) to implement copy on write.
In situation 2 you only have a problem if you try and maintain a 1-1 correspondence between the variable in memory and the database record. Why do you need this correspondence? When you have calculated the amount write it, and if you ever need to look it up, call a database function to find it. If you need to look it up a lot, cache the database function.
Just my opinion, but better to remove the problem in the first place, than to find a way around it. In both these cases you never need to deal with an immutable object.
I clearly do not want RecNo 1 or 2 to be mutable when I pull them into memory.
In fact, even RecNo 3 is immutable in the program, in this sense - if the program wants to change John Smith's state to NY on 6-5-2011, I would make a copy of RecNo 3 and update the database this way: