Painful error with templated template types

So hey. We all know that we can do this, right?

1
2
3
4
5
6
7
8
9
template<typename T, T Lower, T Upper> class RangeCheckedNumber { /* implementation for a class which warns the user if a number of type T goes outside the range of Lower to Upper */ };

typedef RangeCheckedNumber<int, 0, 1> ZeroToOneNumber;

int main(int argc, char** argv)
{
  ZeroToOneNumber num;  // From zero to 1
  return 0;
}


Right, no problem. We're making the type T be an int, and there's also two arguments of type T passed, the lower and upper bounds. One would presumably have an implementation that overrides basic math functions in there and warns the user as stated (this is, in fact, what I'm trying to do).

But what happens when we make one minor tweak and use a float instead of an int?

1
2
3
4
5
6
7
8
9
template<typename T, T Lower, T Upper> class RangeCheckedNumber { /* implementation for a class which warns the user if a number of type T goes outside the range of Lower to Upper */ };

typedef RangeCheckedNumber<float, 0.0f, 1.0f> ZeroToOneNumber;

int main(int argc, char** argv)
{
  ZeroToOneNumber num;  // From zero to 1
  return 0;
}


We get "error: 'float' is not a valid type for a template constant parameter". And that's in line with the standard, which states that only integer types can be used suchly. I understand *why* the standard is like that (aka, the instantiation for RangeCheckedNumber<float, 0.0f, 3.0f> wouldn't necessarily be the same as for RangeCheckedNumber<float, 0.0f, 6.0f / 2.0f>, but I think it's an idiotic decision on their part. That sort of case is rare, the consequences of it are generally minor, and most importantly, anyone who doesn't realize that floating point math has the risk of such problems needs to go back to CS 101.

But I digress.

Obviously floating point math is important with such a class, so we can't just write it off as "can't be done". One solution that came to mind first was passing in the range in the constructor - but that gets absurd if you have to do that *every time* you declare a member of the class (such a class would be pervasive throughout the program). Another solution was to subclass every type. While not as ugly, it's still a horribly ugly solution, every type having to define its own subclass and override the constructor. I also tried using constexpr declarations, but they failed with a bad-grammar error ("X is not a valid template argument for type 'const float' because object 'X' has not external linkage") unless I lied and said that they were externals, and that made me uncomfortable - and it was no better than what I think is the "proper" solution that I found, which is:

1
2
3
4
5
6
7
8
9
10
11
12
template<typename T, const T& Lower, const T& Upper> class RangeCheckedNumber { /* implementation for a class which warns the user if a number of type T goes outside the range of Lower to Upper */ };

float lower = 0.0f;
float upper = 1.0f;

typedef RangeCheckedNumber<float, lower, upper> ZeroToOneNumber;

int main(int argc, char** argv)
{
  ZeroToOneNumber num;  // From zero to 1
  return 0;
}


Compiles, links, runs, no errors. Great. But here's the problem. What I *really* want to be able to do is declare variables like:

1
2
3
4
typedef RangeCheckedNumber<float, 0.0f, 1.0f> ZeroToOneNumber;
typedef RangeCheckedNumber<float, -1.0f, 1.0f> MinusOneToOneNumber;
typedef RangeCheckedNumber<float, 50f, 100f> FiftyToOneHundredNumber;
...


But instead, I must do:

1
2
3
4
5
6
7
float ZeroToOneLower = 0.0f, ZeroToOneUpper = 1.0f;
typedef RangeCheckedNumber<float, ZeroToOneLower, ZeroToOneUpper> ZeroToOneNumber;
float MinusOneToOneLower = -1.0f, MinusOneToOneUpper = 1.0f;
typedef RangeCheckedNumber<float, MinusOneToOneLower, MinusOneToOneUpper> MinusOneToOneNumber;
float FiftyToOneHundredLower = 50.0f, FiftyToOneHundredUpper = 100.0f;
typedef RangeCheckedNumber<float, FiftyToOneHundredLower, FiftyToOneHundredUpper> FiftyToOneHundredNumber;
...


... and so on down the line. Ugly, awkward. But there may be a solution. Is there any sort of std:: functionality that I can use to make the upper and lower bounds inline? Something along the lines of:

1
2
3
4
typedef RangeCheckedNumber<float, std::foo(0.0f), std::foo(1.0f)> ZeroToOneNumber;
typedef RangeCheckedNumber<float, std::foo(-1.0f), std::foo(1.0f)> MinusOneToOneNumber;
typedef RangeCheckedNumber<float, std::foo(50f), std::foo(100f)> FiftyToOneHundredNumber;
...


Is there something like that which exists? Using C++11 is A-okay in my book. :)
Last edited on
> One solution that came to mind first was passing in the range in the constructor
> but that gets absurd if you have to do that *every time* you declare a member of the class
Sorry, I don't follow
how is RangeCheckedNumber<float> ZeroToOneNumber(0,1); //global worst than
1
2
typedef RangeCheckedNumber<float, 0.0f, 1.0f> ZeroToOneNumber;
ZeroToOneNumber num;
Last edited on
You think having to specify a constructor every single time you mention the type, the exact same identical constructor, isn't something that's A) ugly, B) typo-prone, C) makes you have to go back and change everything all throughout the code if the type ever changed instead of just changing the typedef, D) treats all numbers of a given data type (say, everything based on "float") as the same class rather than distinct ones (which partially defeats the purpose and would require the addition all sorts of safeguards to counteract it, as well as making your code less obvious as to what's going on), and on and on? Seems pretty clear to me that would be a god-awful hack.


Last edited on
If you want to check between 0 and 1, then use our `ZeroToOneNumber' object
If for some reason you want to provide state, then clone it auto num = ZeroToOneNumber;
That takes cares of your points A,B and C


As for D, they look the same class to me. They do range checking, and that's all I care.
Moreover, it makes sense to have functions that receive a `checker' as parameter, and I can adjust it at runtime.
"If for some reason"? State is the whole point, the point is to create widely-used objects which function as bounds-checked numbers. And with your solution, I can no longer initialize my variables upon declaration, and have to add a second line (every single time) initializing my variables. In addition to it being a painfully ugly hack.

D: Well, they're not the same. A percentage (0-1) is not an angle (0-2PI) which is not a time which is not a distance which is not.... well, you get the picture. They're totally separate types that should not interact unless you explicitly force them to. You shouldn't be able to set a percentage equal to an angle. A function that returns a time shouldn't be able to return a distance. It should be a syntax error. And it gets nasty to try to fake it at compile time. And of course, E) it makes your code not immediately apparent what you're doing. And even if you typedef a bunch of words to the exact same RangeCheckedNumber<float> and use those typedefs, the compiler is still going to report errors with "RangeCheckedNumber<float>", not the specific type you're using.

I want code that's clear, reliable, and checked.
To put it another way: if your suggestion applied to inbuilt types, instead of being able to do this:

 
int a=5, b=3, c=2;


You're saying I should do this:
1
2
3
4
int a=GenericInt, b=GenericInt, c=GenericInt;
a = 5;
b = 3;
c = 2;


And to illustrate D, well, that's like saying instead of:

1
2
3
4
int a;
float b;
bool c;
char c[4];


I should really be doing

1
2
3
4
5
6
7
8
9
10
11
12
typedef union GenericType
{
    long double d;
    long long int i;
    bool b;
    bool char c[256];
} GenericType;

GenericType a;
GenericType b;
GenericType c;
GenericType d;


Represent the floating point values for the lower bound and upper bound as rational numbers?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <ratio>
#include <type_traits>
#include <limits>
#include <stdexcept>

template< typename FLOAT, std::intmax_t NUM, std::intmax_t DENOM = 1 >
struct fp_as_ratio : std::ratio<NUM,DENOM>
{
    static_assert( DENOM != 0, "infinity/NaN!" ) ;
    using std::ratio<NUM,DENOM>::num ;
    using std::ratio<NUM,DENOM>::den ;
    static constexpr FLOAT value = num / FLOAT(den) ;
};

template< typename FLOAT, std::intmax_t LOWER_NUM, std::intmax_t LOWER_DEN,
                          std::intmax_t UPPER_NUM, std::intmax_t UPPER_DEN >
struct range_checked_fp
{
    static_assert( std::is_floating_point<FLOAT>::value, "not a floating point type!" ) ;

    using limits = std::numeric_limits<FLOAT> ;
    using fp_low_value = fp_as_ratio<FLOAT,LOWER_NUM,LOWER_DEN> ;
    using fp_high_value = fp_as_ratio<FLOAT,UPPER_NUM,UPPER_DEN> ;

    static constexpr FLOAT low_value = fp_low_value::value ;
    static constexpr FLOAT high_value = fp_high_value::value ;
    static constexpr FLOAT epsilon = limits::epsilon() ; // modify as required

    constexpr range_checked_fp() = default ;
    range_checked_fp( FLOAT f ) : value(f)
    { if( !in_range(f) ) throw std::out_of_range( "floating point value is out of range!" ) ; }

    constexpr operator FLOAT() const { return value ; }

    private:
        FLOAT value = fp_low_value::value  ;

        // adjust for epsilon?
        static constexpr bool in_range( FLOAT v ) { return v >= low_value && v <= high_value ; }
} ;

using percentage = range_checked_fp< float, 0, 1, 100, 1 > ; // 0 - 100
using probability = range_checked_fp< double, 0, 1, 1, 1 > ; // 0 - 1

#include <iostream>

int main()
{
    percentage p = 52.3 ;
    std::cout << p << '\n' ;
}

http://ideone.com/DmCCKG
Yeah, I thought about that... it solves some problems but makes some others. Yeah, it would be nearly perfect for allowing floats. It makes the typedefs a bit hard to read, of course. But the bigger problem is that it also unfortunately means that you're forever absolutely restricted to either integer or float types, you could never use a class as the type (for example, range-constrained coordinates with a minimum and maximum coordinate defining the bounding box - and actually, yes, that is a real use case for me).

Really, I think the best solution unless the standard gets changed is probably going to be the one that I proposed in my first post - passing variables by reference as template arguments. But if anyone knows an std function that will inline convert a const into a variable with an external linkage (so that I don't have to add a separate variable declaration line before each typedef), that would make it a lot better, nearly as good as if the C++ standard hadn't included this idiotic restriction to begin with.
I think another way to phrase what would give a good solution would be: "Is there any way (C++11 allowed!) to declare an initialized global variable inline?"

I know with early C++ standards this question would warrant a "ha, you wish" response, but C++11 has all sorts of similar functionality for changing properties of variables and declaring things inline (for example, lambda for inline functions), so I don't think it's unreasonable to ask. Any C++11 experts here?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
template<class T>
class Checker{
	T low, high;
public:
	typedef T value_type;
	Checker(const T &low, const T &high):
		low(low),
		high(high)
		{}
	bool operator()(const T &value){
		return low<=value and value<=high;
	}
};

Checker<float> ZeroToOne(0,1);

template<class Checker, class T>
class Number_with_a_check{
	Checker check;
	T value;
public:
	Number_with_a_check(const Checker &check, const T &value):
		check(check),
		value(value)
		{}
};

template<class Check>
Number_with_a_check<Check,typename Check::value_type> make_number_with_a_check(const Check &check, typename Check::value_type value){
	return Number_with_a_check<Check,typename Check::value_type>(check, value);
}

int main(int argc, char **argv){
	auto n = make_number_with_a_check( ZeroToOne, 0.68 );
}
You could write a user-defined literal, e.g. RangeCheckedNumber<float, 0.0_myf, 1.0_myf> and have it return a type convertible to int with some value that you'd know how to convert back to the float boundary.. but it seems like a rather twisted way to defeat the type system.

incidentally, there is a proposal with the evolution WG to allow any literal type as the type of non-type template parameter: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3413.html which would cover your bounding box example. Last I know, it was reviewed at Bristol meeting deemed incompatible with linking model as-is, but they do want to partially work it in.
ne555: that's not only ugly for each variable declaration (picture if you had to write something like that every time you wanted to use an int), but it's not a type, and so is not a drop-in replacement.

Cubbi: Hmm, interesting concept.... I may investigate that further. Not sure which I'd prefer, my two-lines-per-typedef approach or something like that, but it's a possibility. I don't mind if the code to allow me to do the typedefs is a bit ugly, so long as the typedefs themselves are readible and maintainable (there will be dozens at first, potentially many more down the road) and especially that the usage of the types in the program at large is simple, clear, and reliable.

And big thanks for the link!
Last edited on
Topic archived. No new replies allowed.