C/C++ Users Journal December, 2004
A common scenario that occurs frequently in programming is that of enforcing constraints on a value type, such as to be within a valid range. This is one of those cases where, typically, programmers end up reinventing the wheel each time because a truly general-purpose and reusable solution is not well known. Policy-driven design easily provides a solution to this problem, as it does many others where customizable type behavior is desirable. In this article, I present a general-purpose constrained value type (see Listings 1 and 2) that can be easily modified with policies to adapt to your specific needs.
A few examples of possible candidates for a constrained value type include: days of the week, minutes, seconds, hours, days of the year, months, degrees of a circle, radians, strictly positive values, nonnegative values, probability, percentages, and so on. For example, here I define an integer value type that represents a base 60 number and throws an exception if ever assigned a value out of the range [0..59]. Using the constrained type, it is defined as:
typedef cv::constrained_value <
cv::policies::ranged_integer_constraint
< 0, 59 > > Base60;
Base60 b = 10; // no problems
b = b + 20; // no problems
b = b + 40; // throws exception
Having done this, you can now use Base60 like you would an ordinary int, but any time it is assigned a value below 0 or above 59, an exception is thrown. Of course, throwing an exception is only one possible behavior in the case of a constraint-invalidation condition. The ranged_integer_constraint policy type was written to accept an optional policy on how to deal with invalid conditions. For instance, there is also a saturating range policy, which sets any out-of-range values to the nearest valid value. In this case, that means that values below 0 are set to 0, and values above 59 are set to 59.
typedef cv::constrained_value <
cv::policies::ranged_integer < 0, 59,
cv::policies::saturating > > Base60;
Base60 b = 10; // no problems
std::cout << b << std::endl; // outputs 10
b = b + 70;
std::cout << b << std::endl; // outputs 59
I have only scratched the surface of constrained_value. To really harness the power of constrained_value, you will likely want to write your own constraints policies.
Constraints policies are easy to write. A constraints policy has to include only two componentsa public typedef for a value type named value, and an assign function with the signature: static void assign(const value& rvalue, value& lvalue). Here is a common and simple constraints policy for nonnegative doubles, which throws an empty exception when the double is assigned a value below 0:
struct non_negative_double_constraints {
typedef double value;
static void assign(const value& rvalue,
value& lvalue) {
if (rvalue < 0.0) { throw; }
lvalue = rvalue;
}
}
typedef cv::constrained_value
< non_negative_double_constraint >
non_negative_double;
This defines a new type, non_negative_ double, which is, for the most part, interchangeable with ordinary doubles but throws an exception any time a negative value is assigned.
A more sophisticated constraints policy is one that guarantees the value stays within a valid range by applying a modulo operation. Applying this constraint to the Base60 example would mean that values of 60 become 0, 61 becomes 1, and so on. Here is a constraints policy that achieves just that:
template < int max_T >
struct modulo_integer_constraints {
typedef int value;
static void assign(const value& rvalue,
value& lvalue) {
lvalue = rvalue % max_T;
}
};
typedef cv::constrained_value <
modulo_integer_constraints
< 60 > > Base60;
Base60 b = 10;
std::cout << b << std::endl; // outputs 10
b = b + 70;
std::cout << b << std::endl; // outputs 20
This new Base60 type never throws exceptions and assures you that the value always has a value in the legal range of [0..59].
The constrained_value type is straightforward, as it requires the constraint_policy parameter to do the work of performing assignments and validations according to whatever rules it desires. The constrained_value type defines two local types that it uses for ease of reading and typing: self, which is shorthand for the constrained type itself; and value, which is the value type that is being constrained. The constrained_value type is made up primarily of two initializing constructors and two assignment operators, one each for copying from self or copying from a value. All assignments are routed through to the constraints_policy assign method, which defines whatever particular assignment or validation that is desired. The constrained_value type also includes a conversion operator for value.
One application of the constrained value type is that it allows an easy counter-example to many of the case examples of literature on Design-by-Contract (DbC) techniques, such as the canonical square root problem. The square root problem is simply that any argument to a square root function must be a nonnegative real number. The DbC approach is to apply a precondition to the function, whereas using constrained values, you can simply require that sqrt accept arguments of type non_negative_double. Problem solved.
The design of the constrained value type is based on the original design of the same name by Jeff Garland of http://www.crystalclearsoftware.com/ and suggestions from various members of the Boost mailing list.