Defining Constrained Value Types
Problem
You want self-validating numerical types to represents numbers with a limited range of valid values such as hours of a day or minutes of an hour.
Solution
When working with dates and times, frequently you will want values that are integers with a limited range of valid values (i.e., 0 to 59 for seconds of a minute, 0 to 23 for hours of a day, 0 to 365 for days of a year). Rather than checking these values every time they are passed to a function, you would probably prefer to have them validated automatically by overloading the assignment operator. Since there are so many of these types, it is preferable to implement a single type that can handle this kind of validation for different numerical ranges. Example 5-10 presents a ConstrainedValue template class implementation that makes it easy to define ranged integers and other constrained value types.
Example 5-10. constrained_value.hpp
#ifndef CONSTRAINED_VALUE_HPP #define CONSTRAINED_VALUE_HPP #include #include using namespace std; template struct ConstrainedValue { public: // public typedefs typedef typename Policy_T policy_type; typedef typename Policy_T::value_type value_type; typedef ConstrainedValue self; // default constructor ConstrainedValue( ) : m(Policy_T::default_value) { } ConstrainedValue(const self& x) : m(x.m) { } ConstrainedValue(const value_type& x) { Policy_T::assign(m, x); } operator value_type( ) const { return m; } // uses the policy defined assign function void assign(const value_type& x) { Policy_T::assign(m, x); } // assignment operations self& operator=(const value_type& x) { assign(x); return *this; } self& operator+=(const value_type& x) { assign(m + x); return *this; } self& operator-=(const value_type& x) { assign(m - x); return *this; } self& operator*=(const value_type& x) { assign(m * x); return *this; } self& operator/=(const value_type& x) { assign(m / x); return *this; } self& operator%=(const value_type& x) { assign(m % x); return *this; } self& operator>>=(int x) { assign(m >> x); return *this; } self& operator<<=(int x) { assign(m << x); return *this; } // unary operations self operator-( ) { return self(-m); } self operator+( ) { return self(-m); } self operator!( ) { return self(!m); } self operator~( ) { return self(~m); } // binary operations friend self operator+(self x, const value_type& y) { return x += y; } friend self operator-(self x, const value_type& y) { return x -= y; } friend self operator*(self x, const value_type& y) { return x *= y; } friend self operator/(self x, const value_type& y) { return x /= y; } friend self operator%(self x, const value_type& y) { return x %= y; } friend self operator+(const value_type& y, self x) { return x += y; } friend self operator-(const value_type& y, self x) { return x -= y; } friend self operator*(const value_type& y, self x) { return x *= y; } friend self operator/(const value_type& y, self x) { return x /= y; } friend self operator%(const value_type& y, self x) { return x %= y; } friend self operator>>(self x, int y) { return x >>= y; } friend self operator<<(self x, int y) { return x <<= y; } // stream operators friend ostream& operator<<(ostream& o, self x) { o << x.m; return o; } friend istream& operator>>(istream& i, self x) { value_type tmp; i >> tmp; x.assign(tmp); return i; } // comparison operators friend bool operator<(const self& x, const self& y) { return x.m < y.m; } friend bool operator>(const self& x, const self& y) { return x.m > y.m; } friend bool operator<=(const self& x, const self& y) { return x.m <= y.m; } friend bool operator>=(const self& x, const self& y) { return x.m >= y.m; } friend bool operator==(const self& x, const self& y) { return x.m == y.m; } friend bool operator!=(const self& x, const self& y) { return x.m != y.m; } private: value_type m; }; template struct RangedIntPolicy { typedef int value_type; const static value_type default_value = Min_N; static void assign(value_type& lvalue, const value_type& rvalue) { if ((rvalue < Min_N) || (rvalue > Max_N)) { throw range_error("out of valid range"); } lvalue = rvalue; } }; #endif
The program in Example 5-11 shows how you can use the ConstrainedValue type.
Example 5-11. Using constained_value.hpp
#include "constrained_value.hpp" typedef ConstrainedValue< RangedIntPolicy<1582, 4000> > GregYear; typedef ConstrainedValue< RangedIntPolicy<1, 12> > GregMonth; typedef ConstrainedValue< RangedIntPolicy<1, 31> > GregDayOfMonth; using namespace std; void gregOutputDate(GregDayOfMonth d, GregMonth m, GregYear y) { cout << m << "/" << d << "/" << y << endl; } int main( ) { try { gregOutputDate(14, 7, 2005); } catch(...) { cerr << "whoops, shouldn't be here" << endl; } try { gregOutputDate(1, 5, 1148); cerr << "whoops, shouldn't be here" << endl; } catch(...) { cerr << "are you sure you want to be using a Gregorian Calendar?" << endl; } }
The output from the program in Example 5-11 is:
7/14/2005 are you sure you want to be using a Gregorian Calendar?
Discussion
Constrained value types are particularly relevant when working with dates and times, because many values related to date/times are integers that must occur within a specific range of values (e.g., a month must be in the interval [0,11] or a day of the month must be in the interval [0,30]). It is very time consuming and error prone to manually check that every function parameter fits into a certain range. Just imagine if you wanted to make a global change to how a million line program handled date range errors!
The ConstrainedValue template class when used with a RangedIntPolicy template can be used to define easily several different types that throw exceptions when assigned values out of range. Example 5-12 shows some different examples of how you can use ConstrainedValue to define new self-validating integer types.
Example 5-12. More of usage of ConstrainedValue
typedef ConstrainedValue< RangedIntPolicy <0, 59> > Seconds; typedef ConstrainedValue< RangedIntPolicy <0, 59> > Minutes; typedef ConstrainedValue< RangedIntPolicy <0, 23> > Hours; typedef ConstrainedValue< RangedIntPolicy <0, 30> > MonthDays; typedef ConstrainedValue< RangedIntPolicy <0, 6> > WeekDays; typedef ConstrainedValue< RangedIntPolicy <0, 365 > > YearDays; typedef ConstrainedValue< RangedIntPolicy <0, 51> > Weeks;
The ConstrainedValue template class is an example of policy-based design. A policy is a class passed as a template parameter that specifies aspects of the implementation or behavior of the parameterized type. The policy passed to a ConstrainedValue is expected to provide the implementation detail of how to assign between the same specializations of the type.
Using policies can improve the flexibility of classes by deferring design decisions to the user of the type. It is common to use policies when a group of types has a common interface but vary in their implementation. Policies are also particularly useful when it is impossible to anticipate and satisfy all possible usage scenarios of a given type.
There are many other policies you can possibly use with a ConstrainedValue type. For instance, rather than throw an exception, you may choose to assign a default value, or assign the nearest legal value. Furthermore, constraints don't even have to be ranges: you might even have a constraint that a value is always even.