
On Mon, Dec 1, 2008 at 6:34 PM, Robert Kawulak <robert.kawulak@gmail.com> wrote:
Hi Stjepan,
You have a very nice gift of catching all possible inaccuracies. :D
If I do, it's only because I have a lot of experience in making inaccuracies :-)
From: Stjepan Rajko "It can be used just like the underlying object": I have a suspicion that it can't be used "just like" the underlying object in all circumstances :-) I assume you can't call member functions of the underlying object if it's a class type (with the same syntax), or provide a constrained<int> as an argument to a function that takes an int &.
Of course you're right, this is an informal definition and expresses rather a desire, a design goal, which of course cannot be fully achieved due to the language limitations.
Sure, but I can't guess which limitations you decided not to deal with, and which limitations you cleverly circumvented by making certain assumptions.
Could you provide a slightly more precise explanation?
I'll try. Some hints? ;-)
It seems like we have the following: * the constrained object holds the underlying object * the underlying object can only be given values according to a constraint * the constrained object can replace the underlying object in certain operator expressions (not non-mutating operators, except for stream insertion / extraction) * the constrained object provides const access to the underlying object So, perhaps: Constrained object is a wrapper for another object. It holds the underlying object, and can only be given values which conform to a specified constraint. Thus the set of possible values of a constrained object is a subset of possible values of the underlying object. A constrained object guarantees that its underlying value is constraint-conforming at all times, since its construction until its destruction. The constrained object can be used just like the underlying object in traditionally mutating operator expressions (link to more info) and stream insertions / extractions. It also provides const access to the underlying object via a value() member function. Or, a more compressed version: Constrained object is a wrapper for another object, and can only be given values which conform to a specified constraint. A constrained object guarantees that its underlying value is constraint-conforming at all times, since its construction until its destruction. The constrained object can be used just like the underlying object in certain operator expressions (link to more info), and provides const access to the underlying object via a value() member function. With whatever you choose to go with, I don't mind that it is informal just as long as it not inaccurate or potentially misleading.
*((iter++).value()) ... so I assume you can't do *(iter++). (if so, why not?)
No, you can't. This is because (iter++) is of type constrained<...>, and while it is implicitly convertible to the underlying iterator type, it doesn't have the * operator (actually, it doesn't have any non-mutating operators overloaded). It couldn't have the * operator, because in general case it couldn't know what should be the return type.
Ah, I see. You focused on the typically mutating operators because there you can reasonably assume that the return type should be the constrained object. This would be good to add to the docs, if it's not already there.
"it can be assigned only a value which conforms to a specified constraint": when you say assigned, I'm thinking of the assignment operator, but you constrain more than that. Perhaps there is a more inclusive way of saying this? (maybe "it can only hold values which conform to a specified constraint"?)
Again, you got me here. Maybe "it can only be given values..."? My intention was to stress the fact, that the constraint checking happens each time the object is actually modified (and it is usually modified through the assignment operators, although not exclusively).
I like that better.
In your example "Object remembering its past extreme values", the policy is changing the constraint object directly. But, in your tutorial, you have: "Constraint of a constrained object cannot be accessed directly for modification, because the underlying value could become invalid according to the modified constraint. Therefore the constraint of a constrained object is immutable and change_constraint() function has to be used in order to modify the constraint. ..." Is the example violating how the library should be used?
No. From the perspective of a constrained object's user it's true that the constraint cannot be accessed directly for modification in any way. OTOH the error policy is allowed to modify anything within the constrained object when invoked (as long as the value remains constraint-conforming). This is what the policy in the example does.
OK, that makes sense. The policy is the one place that guarantees to leave the object in a valid constrained state, so it is the one place that is allowed to directly change the constraint. This would also be good to mention or reference when you talk about change_constraint (since as a user of the constrained object, I could be providing the policy myself).
The value() function returns the underlying object by const &... so, I'm assuming that the constraint is not allowed to depend on any mutable parts of the underlying object's state?
The constraint may depend on any state, mutable or not -- it's the constrained object's task to make sure that the value is immutable for the "outside world" (and it does so by providing only value access methods returning a const reference).
Sorry, I meant `mutable` as in the mutable keyword. For example: struct observable_int { // initialization omitted int observe() const { m_times_observed++; return m_value; } unsigned times_observed() const { return m_times_observed; } private: int m_value; mutable unsigned m_times_observed; // initialized to 0 } // One could think that this would be a reasonable constraint struct is_viewed_few_times { bool operator () (const observable_int &x) const { return x.times_observed()<10; } }; constrained<observable_int, is_viewed_few_times> x; // but it is not enforced for(int i=0; i<20; i++) x.value().observe(); // never complains Speaking of access to the underlying object in situations where you need non-const access to it... you could provide a member function that takes a unary Callable as a parameter, and calls the Callable with a copy of the underlying object as the argument. After the call returns, it assigns the (perhaps modified) value of the copy back to the underlying object (through the policy / checking the constraint). AFAICT, your guarantee is still never violated, and this would provide a really useful piece of functionality. Instead of using a copy you could also use the underlying object as the argument directly, but that weakens your guarantee (and if the Callable keeps an address of the object, throws the guarantee out the window). Best, Stjepan