
Brian Martin wrote:
I'll have another think about some of the compile time check issues.
I have and have done the following: 1) Compile time checks would be nice but are basically impossible. At least the ones that count. Such as: subrange<int, 1, 20> sub_20(30); or sub_20 = 300; Pascal traps this kind of thing but C++ can't, as far as I can see. Runtime only, I'm afraid. I am of the opinion that the compile time checks that were given in Maciej's example code are of limited use since the real benefit of subranges is runtime (in my humble opinion). In the example code, it was possible to assign/copy different subrange types, provided the source had a range inside the destination. For now, my subrange doesn't allow different subrange types to mix. 2) Agonised over whether or not to remove the typename T (I call it a "base type" a la Pascal terminology) from the template. In my previous post I said that it allows any type to be stored in the subrange, forgetting of course that you can't have doubles, classes and C strings as non-type template parameters. Doh! Actually, this is what I want since subranges of classes is a bit odd. Floating point subranges would have been nice, but Pascal doesn't allow them either. There is a way of doing it, but it messes up the syntax. I've kept the base type because you waste storage if you're storing anything smaller than a long. There's no class overhead (on my compiler) so this doesn't make the wastage trivial. The waste is ok if you're only using a few subranges but in one project I'm storing potentially large numbers of subranges (of shorts) in STL containers and then reading/writing them in binary from/to files. Didn't like the files being 2 times bigger than they should be. So, reluctantly, I've kept the base type. 3) I've used a conversion operator. I usually avoid these but in this case it integrates subranges nicely into the type system so that built in types and subranges can mix freely in expressions. The template is, after all, supposed to be a (restricted) integral type. 4) Removed operator~(). It always fails since the value gets its upper bits set to 1 which will exceed the subrange unless you've set the upper limit to the maximum the type can store. There are obvious problems with signed types too. 6) Have attached some code - the subrange class itself and a test program for anyone who wants to give it a go.

Sorry to come late to this discussion, busy, busy... In the date_time library there is a class called constrained_value that does something very similar to what you are attempting. It uses a policy to define the error handling -- so you throw an exception or whatever. In the case of date_time it is used exclusively during construction to do range checking of input. So the class doesn't support arithmetic operators. By creating a bunch of these small utitlity classes almost all user input checking is performed transparently by simple construction. That is: //actually constructs 3 objects to check range of year, month, day date d(2004, 4, 1); So I think you can argue that there is some use to the subrange idea. Here's the details on how it gets used: //from boost/date_time/greg_month.hpp //! Exception thrown if a greg_month is constructed with a value out of range struct bad_month : public std::out_of_range { bad_month() : std::out_of_range(std::string("Month number is out of range 1..12")) {} }; //! Build a policy class for the greg_month_rep typedef CV::simple_exception_policy<unsigned short, 1, 12, bad_month> greg_month_policies; //! A constrained range that implements the gregorian_month rules typedef CV::constrained_value<greg_month_policies> greg_month_rep; And here's the code that implements it. //from boost/date_time/constrained_value.hpp namespace boost { //! Namespace containing constrained_value template and types namespace CV { //! Represent a min or max violation type enum violation_enum {min_violation, max_violation}; //! A template to specify a constrained basic value type /*! This template provides a quick way to generate * an integer type with a constrained range. The type * provides for the ability to specify the min, max, and * and error handling policy. * * <b>value policies</b> * A class that provides the range limits via the min and * max functions as well as a function on_error that * determines how errors are handled. A common strategy * would be to assert or throw and exception. The on_error * is passed both the current value and the new value that * is in error. * */ template<class value_policies> class constrained_value { public: typedef typename value_policies::value_type value_type; // typedef except_type exception_type; constrained_value(value_type value) { assign(value); }; constrained_value& operator=(value_type v) { assign(v); return *this; } //! Return the max allowed value (traits method) static value_type max BOOST_PREVENT_MACRO_SUBSTITUTION () {return (value_policies::max)();}; //! Return the min allowed value (traits method) static value_type min BOOST_PREVENT_MACRO_SUBSTITUTION () {return (value_policies::min)();}; //! Coerce into the representation type operator value_type() const {return value_;}; protected: value_type value_; private: void assign(value_type value) { //adding 1 below gets rid of a compiler warning which occurs when the //min_value is 0 and the type is unsigned.... if (value+1 < (min)()+1) { value_policies::on_error(value_, value, min_violation); return; } if (value > (max)()) { value_policies::on_error(value_, value, max_violation); return; } value_ = value; } }; //! Template to shortcut the constrained_value policy creation process template<typename rep_type, rep_type min_value, rep_type max_value, class exception_type> class simple_exception_policy { public: typedef rep_type value_type; static rep_type min BOOST_PREVENT_MACRO_SUBSTITUTION () { return min_value; }; static rep_type max BOOST_PREVENT_MACRO_SUBSTITUTION () { return max_value;}; static void on_error(rep_type, rep_type, violation_enum) { throw exception_type(); } }; } } //namespace CV

Hi, Brian Martin wrote:
Brian Martin wrote:
I'll have another think about some of the compile time check issues.
I have and have done the following:
1) Compile time checks would be nice but are basically impossible. At least the ones that count. Such as:
subrange<int, 1, 20> sub_20(30); or sub_20 = 300;
Pascal traps this kind of thing but C++ can't, as far as I can see. Runtime only, I'm afraid.
One could also try to do: subrange<int, 1, 20> sub_20 = literal<int, 20>(); sub_20 = literal<int, 300>(); // should not compile with compile-time checks where literal can be as simple as: template <typename T, T N> struct literal {}; + a couple of overloads in the subrange class.
I am of the opinion that the compile time checks that were given in Maciej's example code are of limited use since the real benefit of subranges is runtime (in my humble opinion).
It all depends on what you want to achieve. Compile-time checks allow you to *avoid* any checking at run-time, thus making the whole subrange class (or however you call it) potentially the same efficient as the raw fundamental types. Most of the optimizers are able to: 1. inline simple function calls and 2. eliminate unused code, including creation and destruction of unused local objects With both of these optimizations in hand, the following code: s1 = s2 + s3; can be potentially reduced to a few machine instructions, just as if s1, s2 and s3 were just ints or longs or whatever. I think that the range class should not impose any cost (runtime-check) in the places where it can be proved that such cost can be avoided. The second advantage of using compile-time checks is to find places that will for sure break the code, for example where source and destination ranges do not overlap. There is no reason to let such code compile, because it may form the execution path that you will never be able to test by yourself and your will get the error at your most important presentation. As usual in such cases. In places where compile-time checks cannot be made, run-time checks (possibly followed by exceptions) are used. This includes assignments from rvalues of fundamental type. This is the most optimal solution in my opinion.
3) I've used a conversion operator.
Me too but removed it. It broke (introduced ambiguities) other overloaded operators. Try to write some more complicated expression involving both ranges and fundamental types, function calls, etc. This is a good test.
4) Removed operator~().
I resigned from other bitwise operators (including shifts) as well. -- Maciej Sobczak : http://www.msobczak.com/ Programming : http://www.msobczak.com/prog/
participants (3)
-
Brian Martin
-
Jeff Garland
-
Maciej Sobczak