Boost logo

Boost :

Subject: Re: [boost] [review][constrained_value] Review of Constrained ValueLibrary begins today
From: Stjepan Rajko (stjepan.rajko_at_[hidden])
Date: 2008-12-02 08:10:58


On Mon, Dec 1, 2008 at 6:34 PM, Robert Kawulak <robert.kawulak_at_[hidden]> 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


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk