[ ... ]
It's worth observing, though, that which values that can be assigned to the
variable in question often depends on the containing object.
At least in my experience, it often depends on the containing _class_,
and less often on the actual containing object (i.e. the instance as
opposed to the class). That may easily be what you meant, but (at least
to me) it's a significant distinction.
If that's not
the case in a particular instance, then so be it, but if it is then you
would be unwise to make the variable public. The key phrase in what you were
saying is "something that already provides suitable control" - a class
doesn't know what other class might contain an instance of it, so it
literally can't provide such suitable control. Only the containing class
knows the restrictions which need to be applied to the contained instance.
IME, at least 80% of the time, all that's really needed is fairly simple
range-checking. In this case, it's pretty easy to isolate the work into
one small, fairly simple class. (I've posted roughly similar code to
this previously, but this is slightly different from them -- this
supports floating point types, and finally gets around to fixing a
problem I introduced years ago, of throwing an exception upon attempting
to extract an out-of-range value, rather than setting the stream's fail
bit as it should).
#include <exception>
#include <iostream>
#include <functional>
#include <ios>
template <class T, class less=std::less<T> >
class bounded {
const T lower_, upper_;
T val_;
bool check(T const &value) {
return less()(value, lower_) || less()(upper_, value);
}
void assign(T const &value) {
if (check(value))
throw std::domain_error("Out of Range");
val_ = value;
}
public:
bounded(T const &lower, T const &upper)
: lower_(lower), upper_(upper) {}
bounded(bounded const &init) { assign(init); }
bounded &operator=(T const &v) { assign(v); return *this; }
operator T() const { return val_; }
friend std::istream &operator>>(std::istream &is, bounded &b) {
T temp;
is >> temp;
if (b.check(temp))
is.setstate(std::ios::failbit);
else
b.val_ = temp;
return is;
}
};
Now, the parent class uses something like:
struct geoPosition {
bounded<double> latitude(-90.0, 90.0);
bounded<double> longitude(-180.0, 180.0);
// ...
};
The range is now directly documented in the definition, rather than
being spread throughout the code of the owning class. This code itself
is simple enough that testing and verification is trivial, and it
isolates the range checking, so changes in the owning class can't
accidentally by-pass the range-checking anywhere, or things like that.