Hi Robert, It is great to see this library getting into Boost. It fills an important gap, but it is only after a long while that I realized that, because the introductory part of the documentation does not stress it enough, and in fact it misled me a bit. My understanding is that safe<int> represents a high level abstraction of an integral number from mathematics: unlike raw type int, which represents a set of allowed operations on the underlying processor. You could say that raw type 'int' is only needed to efficiently implement higher level abstractions, like BigInt or safe<int> -- the two only differ in trade-offs between efficiency and the range of values they can represent. I was missing this in the introduction. Instead I got a somewhat negative impression that the goal of the library is to turn any UB into throwing exceptions (and thereby prevent any UB-based static analysis from detecting programmer errors). I would like to see the library in Boost, although I must admit the other parts do not sound that compelling as safe<int>. Regards, &rzej
On 12/11/15 7:07 AM, Andrzej Krzemienski wrote:
Hi Robert,
It is great to see this library getting into Boost. It fills an important gap, but it is only after a long while that I realized that, because the introductory part of the documentation does not stress it enough, and in fact it misled me a bit.
I'm not sure if you're referring to the documentation as it was when you posted your review or the documentation in it's current state. When I read you're review I realized that I hadn't clearly isolated the concept of undefined behavior from that if incorrect arithmetic behavior. I reworked a lot of the documentation based on your review. Also as I worked on the package it became clear to me that I had to be more ambitious to make the impact I wanted.
My understanding is that safe<int> represents a high level abstraction of an integral number from mathematics: unlike raw type int, which represents a set of allowed operations on the underlying processor.
Very well put. I'll work that in somewhere.
You could say that raw type 'int' is only needed to efficiently implement higher level abstractions, like BigInt or safe<int> -- the two only differ in trade-offs between efficiency and the range of values they can represent.
Again correct. The key concept is that our machines don't implement arithmetic. It's up to us to find a way to make them do it. Until we do, we'll be in a continuous battle to reconcile the differences between the arithmetic we think about and the bit manipulation that our machines do.
I was missing this in the introduction. Instead I got a somewhat negative impression that the goal of the library is to turn any UB into throwing exceptions (and thereby prevent any UB-based static analysis from detecting programmer errors).
That wasn't all that in accurate. When I started the library, I was just thinking I needed a hack to reduce my bug count. I figured I could make a small class which would do the job and that it take a couple of days to make a cool little class. That was three years ago. As I started to patch up corner cases, I came to understand that one had to grab the stick from the other end. That is, instead of trying to patch up C/C++ arithmetic to make it less error prone, start with arithmetic and "re-implement" it in terms of the operations that computer hardware actually implements. Add to this two key requirements: a) do not ignore cases where the underlying hardware cannot produce a correct result. b) implementation must be as fast as possible
I would like to see the library in Boost, although I must admit the other parts do not sound that compelling as safe<int>.
what other parts?
Regards, &rzej
_______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost
2015-12-11 17:21 GMT+01:00 Robert Ramey
On 12/11/15 7:07 AM, Andrzej Krzemienski wrote:
Hi Robert,
It is great to see this library getting into Boost. It fills an important gap, but it is only after a long while that I realized that, because the introductory part of the documentation does not stress it enough, and in fact it misled me a bit.
I'm not sure if you're referring to the documentation as it was when you posted your review or the documentation in it's current state. When I read you're review I realized that I hadn't clearly isolated the concept of undefined behavior from that if incorrect arithmetic behavior. I reworked a lot of the documentation based on your review.
Also as I worked on the package it became clear to me that I had to be more ambitious to make the impact I wanted.
Yes, I am referring to the rewritten documentation. I observe that it has changed significantly.
My understanding is that safe<int> represents a high level abstraction of an integral number from mathematics: unlike raw type int, which represents a set of allowed operations on the underlying processor.
Very well put. I'll work that in somewhere.
You could say that raw type 'int' is only needed to efficiently implement
higher level abstractions, like BigInt or safe<int> -- the two only differ in trade-offs between efficiency and the range of values they can represent.
Again correct. The key concept is that our machines don't implement arithmetic. It's up to us to find a way to make them do it. Until we do, we'll be in a continuous battle to reconcile the differences between the arithmetic we think about and the bit manipulation that our machines do.
I was missing this in the introduction. Instead I got a somewhat negative
impression that the goal of the library is to turn any UB into throwing exceptions (and thereby prevent any UB-based static analysis from detecting programmer errors).
That wasn't all that in accurate. When I started the library, I was just thinking I needed a hack to reduce my bug count. I figured I could make a small class which would do the job and that it take a couple of days to make a cool little class.
That was three years ago.
As I started to patch up corner cases, I came to understand that one had to grab the stick from the other end. That is, instead of trying to patch up C/C++ arithmetic to make it less error prone, start with arithmetic and "re-implement" it in terms of the operations that computer hardware actually implements. Add to this two key requirements:
a) do not ignore cases where the underlying hardware cannot produce a correct result.
b) implementation must be as fast as possible
I was missing such strong statement from the first page: "This library does not avoid some UB: it provides a higher level abstraction than 'int' even if it has the same interface".
I would like to see the library in Boost, although I must admit the other
parts do not sound that compelling as safe<int>.
what other parts?
1. safe_unsigned_range -- while I understand the idea behind it, I don't think I would be inclined to use it. And also, I believe it addresses a different problem, and could be handled by another library, like: http://rk.hekko.pl/constrained_value/ 2. safe<unsigned int> -- this looks suspicious. Unlike signed int, unsigned int does not overflow. It is meant to represent the modulo arithmetic with well defined result for any input values. If I do not want modulo arithmetic, I would rather go with safe<int> than safe<unsigned>. 3. The docs mention a possible extension for safe<float>. I do not know what that would mean. In the case of safe<int> I know what to expect: you trap when I try to build value greater than INT_MAX or smaller than INT_MIN: nothing else could go wrong. But in case of float: do you trap when I want to store: safe<float> ans = safe<float>(1.0) / safe<float>(3.0); ?? 1/3 does not have an accurate representation in type float. But if you trap that, you will be throwing exceptions all the time. So is it only about overflow? Regards, &rzej
On Dec 11, 2015, at 11:11 PM, Andrzej Krzemienski
wrote: 3. The docs mention a possible extension for safe<float>. I do not know what that would mean. In the case of safe<int> I know what to expect: you trap when I try to build value greater than INT_MAX or smaller than INT_MIN: nothing else could go wrong. But in case of float: do you trap when I want to store:
safe<float> ans = safe<float>(1.0) / safe<float>(3.0);
?? 1/3 does not have an accurate representation in type float. But if you trap that, you will be throwing exceptions all the time. So is it only about overflow?
yes we do, and no we don’t… In the case of safe<foat>, different scenarios lead to different requirements. For example, in some contexts overflow to infinite is ok, but rounding not. In others rounding is ok, but overflow and underflow not. In others Pole-errors are okay, but domain errors not… Etc… plenty of use-cases… The approach we took (still experimenting on it) is delegating the responsibility of deciding the “safeness” requirements to the user, and enforce whatever the user requires. Thus, the datatype receives 4 template parameters: — The datatype to wrap (float, double…) — The list of checks to enforce (no-rounding, no-overflow, no-underflow, safe-cast, …) — The way to report a failure (exceptions, errors, abort, assert, report to log….) — The casting policies (never-cast, safe-cast, ... ). Some of the policies are mentioned here: http://sdavtaker.github.io/safefloat/doc/html/types.html http://sdavtaker.github.io/safefloat/doc/html/types.html That documentation is a little old and the way we implemented things changed a lot. However, the list of things we are checking stayed pretty stable.
what other parts?
1. safe_unsigned_range -- while I understand the idea behind it, I don't think I would be inclined to use it.
And also, I believe it addresses a
different problem, and could be handled by another library, like: http://rk.hekko.pl/constrained_value/
I don't remember seeing this before. Clearly there is some overlap.
2. safe<unsigned int> -- this looks suspicious. Unlike signed int, unsigned int does not overflow.
It does overflow. The only difference is that it doesn't result in undefined behavior. The behavior is still arithmetically incorrect though.
It is meant to represent the modulo arithmetic with well defined result for any input values.
Hmmm - I didn't believe that. But I checked and found that std::numeric_limits<unsigned int>::is_modulo is set to true. I'm going to argue that many programmers use unsigned as a number which can only be positive and do not explicitly consider the consequences of overflow. In order to make a drop in replace which is safe, we need this. BTW - the utility of using a built-in unsigned as a modulo integer which a specific number of bits is pretty small. If I want to a variable to hold the minutes in an hour or eggs in a dozen it doesn't help me. I'm doubting that unsigned is use as a modulo integer in only a few very odd cases - and of course one wouldn't not apply "safe" in these cases.
If I do not want modulo arithmetic, I would rather go with safe<int> than safe<unsigned>.
You might not have a choice. If you're re-working some existing program which uses the unsigned range, you'll need this. In the context of this library the safe_range ... are important for a very special reason. The bounds are carried around with type of expression results. So if I write save<int> a, x, b, y; y = a * x + b; runtime checking will generally have to be performed. But if I happen to know that my variables are limited to certain range. safe_integer_range<-100, 100> a, x, b, y; y = a * x + b; Then it can be known at compile time that y can never overflow so no runtime checking is required. Here we've achieved the holy grail: a) guaranteed correct arithmetic result b) no runtime overhead. c) no exception code emitted. d) no special code - we just write algebraic expressions This is the true motivation for safe_..._range
3. The docs mention a possible extension for safe<float>. I do not know what that would mean. In the case of safe<int> I know what to expect: you trap when I try to build value greater than INT_MAX or smaller than INT_MIN: nothing else could go wrong. But in case of float: do you trap when I want to store:
safe<float> ans = safe<float>(1.0) / safe<float>(3.0);
?? 1/3 does not have an accurate representation in type float. But if you trap that, you will be throwing exceptions all the time. So is it only about overflow?
The whole question of what a safe<float> means is being explored. Clearly there is a case where would want to handle divide by zero without crashing the program. The base situation is where an operation results in a Nan. Most current implementation don't trap and just soldier on propagating the Nan. I've never felt comfortable with this - it's not arithmetic any more. I don't want to say much about this because it's a deep subject and I know enough about to know that I don't want to say anything about it. Robert Ramey
2015-12-12 1:18 GMT+01:00 Robert Ramey
what other parts?
1. safe_unsigned_range -- while I understand the idea behind it, I don't think I would be inclined to use it.
And also, I believe it addresses a
different problem, and could be handled by another library, like: http://rk.hekko.pl/constrained_value/
I don't remember seeing this before. Clearly there is some overlap.
2. safe<unsigned int> -- this looks suspicious. Unlike signed int, unsigned
int does not overflow.
It does overflow. The only difference is that it doesn't result in undefined behavior. The behavior is still arithmetically incorrect though.
It is meant to represent the modulo arithmetic with
well defined result for any input values.
Hmmm - I didn't believe that. But I checked and found that std::numeric_limits<unsigned int>::is_modulo is set to true.
I'm going to argue that many programmers use unsigned as a number which can only be positive and do not explicitly consider the consequences of overflow. In order to make a drop in replace which is safe, we need this.
They do. And they get themselves into many troubles. E.g.: unsigned i = -1; They also loose the UB, and therefore loose the chance of a good compiler or static analyzer detecting some bugs.
BTW - the utility of using a built-in unsigned as a modulo integer which a specific number of bits is pretty small. If I want to a variable to hold the minutes in an hour or eggs in a dozen it doesn't help me. I'm doubting that unsigned is use as a modulo integer in only a few very odd cases - and of course one wouldn't not apply "safe" in these cases.
True. The advice I have heard quite often now is: use unsigned only for bitmasks.
If I do not want modulo
arithmetic, I would rather go with safe<int> than safe<unsigned>.
You might not have a choice. If you're re-working some existing program which uses the unsigned range, you'll need this.
Agreed.
In the context of this library the safe_range ... are important for a very special reason. The bounds are carried around with type of expression results. So if I write
save<int> a, x, b, y; y = a * x + b;
runtime checking will generally have to be performed. But if I happen to know that my variables are limited to certain range.
safe_integer_range<-100, 100> a, x, b, y; y = a * x + b;
Then it can be known at compile time that y can never overflow so no runtime checking is required. Here we've achieved the holy grail:
a) guaranteed correct arithmetic result b) no runtime overhead. c) no exception code emitted. d) no special code - we just write algebraic expressions
How can this be possible? If I assign: a = 100; x = 100; b = 100; then (a * x + b) results 10100 and will overflow upon assignment to y. Or am I missing something? I can imagine that the following works though: safe_integer_range<-100, 100> a, x, b; safe_integer_range<-10100, 10100> y = a * x + b; Regards, &rzej
On 12/11/15 4:54 PM, Andrzej Krzemienski wrote:
In the context of this library the safe_range ... are important for a very special reason. The bounds are carried around with type of expression results. So if I write
save<int> a, x, b, y; y = a * x + b;
runtime checking will generally have to be performed. But if I happen to know that my variables are limited to certain range.
safe_integer_range<-100, 100> a, x, b, y; y = a * x + b;
Then it can be known at compile time that y can never overflow so no runtime checking is required. Here we've achieved the holy grail:
a) guaranteed correct arithmetic result b) no runtime overhead. c) no exception code emitted. d) no special code - we just write algebraic expressions
How can this be possible? If I assign:
a = 100; x = 100; b = 100;
then (a * x + b) results 10100 and will overflow upon assignment to y. Or am I missing something?
whoops - need to make an adjustment using safe_t = safe_integer_range<-100, 100, native, trap_exception>; safe_integer_range<-100, 100> a, x, b, y; y = a * x + b; at compile time it detected that the result falls in the range [-10100, 10100] and that this range is not included in the range [100, 100]. The trap_exception policy will invoke a static_assert. Robert Ramey
On 12/11/15 5:15 PM, Robert Ramey wrote:
using safe_t = safe_integer_range<-100, 100, native, trap_exception>;
safe_t a, x, b, y; y = a * x + b;
at compile time it detected that the result falls in the range [-10100, 10100] and that this range is not included in the range [100, 100]. The trap_exception policy will invoke a static_assert.
Robert Ramey
whoops should be: using safe_t = safe_integer_range<-100, 100, native, trap_exception>; safe_t a, x, b, y; y = a * x + b;
safe_integer_range<-100, 100> a, x, b, y; y = a * x + b;
Then it can be known at compile time that y can never overflow so no runtime checking is required.
This is the second time you've written something like this - in the PDF you said that squaring an int8_t didn't overflow. clearly it depends on the values of a,x,b which in general are only known at runtime.. So what do you mean there are no runtime checks?
On 12/11/15 5:01 PM, Pete Bartlett wrote:
safe_integer_range<-100, 100> a, x, b, y; y = a * x + b;
Then it can be known at compile time that y can never overflow so no runtime checking is required.
This is the second time you've written something like this - in the PDF
The example in the proposal was confusing - I've since corrected it. you said that squaring an int8_t didn't overflow. int8_t x = 100 int y = x * x; // x * x cannot overflow clearly it depends on the values of a,x,b which in general are only known at runtime. So what do you mean there are no runtime checks? The reason x * x cannot overflow is the C++ type promotion rules for expressions. signed operands in binary expressions which are smaller than than int are promoted to int. Then the operation (multiplication is performed. Since the maximum value that int8_t can hold is 255 and 255 * 255 is less than the maximum value that an int can hold, the multiplication can never overflow and there is no need to check it. Robert Ramey
On 12/11/15 5:50 PM, Robert Ramey wrote: To belabor the point, consider this little program: #include <iostream> #include <cstdint> using namespace std; int main(){ int8_t x = 100; int y = x * x; cout << y << endl; uint32_t z1 = 100; int8_t z2 = -100; auto y2 = z1 * z2; cout << y2 << endl; return 0; } which prints out: 10000 4294957296 This is due to the application of the C++ type promotion rules. Is it any reason that C++ drives people crazy? One looks at the program and sees arithmetic. One executes the program an gets ... well I don't know what you're getting. Robert Ramey
Hello, On Fri, Dec 11, 2015 at 06:10:01PM -0800, Robert Ramey wrote:
On 12/11/15 5:50 PM, Robert Ramey wrote: To belabor the point, consider this little program:
#include <iostream> #include <cstdint> using namespace std; int main(){ int8_t x = 100; int y = x * x; cout << y << endl; uint32_t z1 = 100; int8_t z2 = -100; auto y2 = z1 * z2; cout << y2 << endl; }
which prints out:
10000 4294957296
This is due to the application of the C++ type promotion rules. Is it any reason that C++ drives people crazy?
This is the typical behaviour of C++ programmers, which are always hypercritical about the language, in stark contrast to any other programming language. The only problem I see above is that the compiler should issue a warning for the auto-line. And any reasonable programmer should not write such code. Oliver
On 12/11/15 6:47 PM, Oliver Kullmann wrote:
Hello,
On Fri, Dec 11, 2015 at 06:10:01PM -0800, Robert Ramey wrote:
On 12/11/15 5:50 PM, Robert Ramey wrote: To belabor the point, consider this little program:
#include <iostream> #include <cstdint> using namespace std; int main(){ int8_t x = 100; int y = x * x; cout << y << endl; uint32_t z1 = 100; int8_t z2 = -100; auto y2 = z1 * z2; cout << y2 << endl; }
which prints out:
10000 4294957296
This is due to the application of the C++ type promotion rules. Is it any reason that C++ drives people crazy?
This is the typical behaviour of C++ programmers, which are always hypercritical about the language, in stark contrast to any other programming language.
The only problem I see above is that the compiler should issue a warning for the auto-line. And any reasonable programmer should not write such code.
This is actually the fundamental issue. Shouldn't one be able to just look at an arithmetical statement and expect it to produce a correct arithmetical result? Should one be required to keep in mind that even though it looks like arithmetic and it was designed to look like arithmetic on purpose there are a bunch of extra-arithmetical rules (buried in the C++ standard) which one has to keep in mind to verify that each statement is correct? I think your point of view is typical of C/C++ programmers and in fact of computer programmers in general. It's the view what we're not doing math we're operating a machine. If that's our view we shouldn't even use C++ expressions but rather stick to assembler where there is no pretense that we're evaluating arithmetical expressions. The reason programming languages were invented is so that we than think in higher abstractions. In this case we want to think in terms of algebra and arithmetic - and the way C++/C is implemented inhibiting us from doing that. It's holding us back. If it's any comfort, it seems that all computer languages - even interpreted ones - suffer from this problem. It's amazing to me that 50 years of computer software evolution - and we're still living with this. This is my main motivation for this effort. Robert Ramey
2015-12-12 3:10 GMT+01:00 Robert Ramey
On 12/11/15 5:50 PM, Robert Ramey wrote: To belabor the point, consider this little program:
#include <iostream> #include <cstdint>
using namespace std;
int main(){ int8_t x = 100; int y = x * x; cout << y << endl;
uint32_t z1 = 100; int8_t z2 = -100; auto y2 = z1 * z2; cout << y2 << endl;
return 0; }
which prints out:
10000 4294957296
This is due to the application of the C++ type promotion rules.
Is it any reason that C++ drives people crazy?
While I do agree with your point of view, I do not agree with the choice of the example. I would say that the problem here comes from the fact that that an unsigned type is used for anything else but a bit-set. To fix it, and to avoid such problems in the future, one does not necessarily have to use a library, but simply apply the rule "never use unsigned to represent integer numbers, even the positive ones". And it would work for some time, until I start playing with bigger numbers: int main(){ int8_t x = 100; int y = x * x; std::cout << y << std::endl; int z1 = 1000000000; int z2 = 1000000000; auto y2 = z1 * z2; // overflow std::cout << y2 << std::endl; return 0; } And at this point I need safe<int> (or a BigInt). Regards, &rzej
On 12/14/15 12:31 AM, Andrzej Krzemienski wrote:
While I do agree with your point of view, I do not agree with the choice of the example. I would say that the problem here comes from the fact that that an unsigned type is used for anything else but a bit-set. To fix it, and to avoid such problems in the future, one does not necessarily have to use a library, but simply apply the rule "never use unsigned to represent integer numbers, even the positive ones".
My library does not purport to "fix" any problems - only detect them. So you might think of my library as a way to detect violations in your rules. I don't see anything in the library which would have to be changed in order to serve your stated purpose.
And it would work for some time, until I start playing with bigger numbers:
int main(){ int8_t x = 100; int y = x * x; std::cout << y << std::endl;
int z1 = 1000000000; int z2 = 1000000000; auto y2 = z1 * z2; // overflow std::cout << y2 << std::endl;
return 0; }
And at this point I need safe<int>
well, feel free to use it! (or a BigInt). Which to me is an entirely different thing for a different purpose. The main purpose of the safe numeric library is to reconcile the conflict between what people expect when they see an arithmetic expression and what current computer languages actually do. BigInt - or John Maddox's multi-precision numerics address an entirely different concern. C++ doesn't support arbitrarily large numbers and those libraries do. You could say that all libraries also address the confict - but they do so in a very different way. So one or the other is a better choice depending on the purpose to which they are to be used. These libraries complement each other. Robert Ramey
2015-12-12 1:18 GMT+01:00 Robert Ramey
In the context of this library the safe_range ... are important for a very special reason. The bounds are carried around with type of expression results. So if I write
save<int> a, x, b, y; y = a * x + b;
runtime checking will generally have to be performed. But if I happen to know that my variables are limited to certain range.
safe_integer_range<-100, 100> a, x, b, y; y = a * x + b;
Then it can be known at compile time that y can never overflow so no runtime checking is required. Here we've achieved the holy grail:
a) guaranteed correct arithmetic result b) no runtime overhead. c) no exception code emitted. d) no special code - we just write algebraic expressions
This is the true motivation for safe_..._range
Ok, now I think I understand the scope of the library. You give me the tool
for representing a mathematical notion of an integral number within *some*
limited range. The contract is:
You guarantee:
1. Either correct result or a compile time error or a run-time exception.
2. No memory management: you will only take the space of one scalar type.
I specify the range of the representable values. I can say:
1. "From -100 to +100", or
2. "Whatever range 'int' as on my machine".
But if I got it right, I would say that the choice of the name, and the
interface do not indicate the intent as clear as they could. Imagine the
following alternative interface:
small_int
On December 14, 2015 3:43:25 AM EST, Andrzej Krzemienski
2015-12-12 1:18 GMT+01:00 Robert Ramey
: In the context of this library the safe_range ... are important for a very special reason. The bounds are carried around with type of expression results. So if I write
save<int> a, x, b, y; y = a * x + b;
runtime checking will generally have to be performed. But if I happen to know that my variables are limited to certain range.
safe_integer_range<-100, 100> a, x, b, y; y = a * x + b;
Then it can be known at compile time that y can never overflow so no runtime checking is required. Here we've achieved the holy grail:
a) guaranteed correct arithmetic result b) no runtime overhead. c) no exception code emitted. d) no special code - we just write algebraic expressions
This is the true motivation for safe_..._range
Why isn't that the behavior of your safe type in the first place? That is, what benefit does your safe type offer that it shouldn't just be supplanted by the range type?
Ok, now I think I understand the scope of the library. You give me the tool for representing a mathematical notion of an integral number within *some* limited range. The contract is:
You guarantee: 1. Either correct result or a compile time error or a run-time exception. 2. No memory management: you will only take the space of one scalar type.
I specify the range of the representable values. I can say: 1. "From -100 to +100", or 2. "Whatever range 'int' as on my machine".
But if I got it right, I would say that the choice of the name, and the interface do not indicate the intent as clear as they could. Imagine the following alternative interface:
small_int
>; // range-based policy small_int ; // underlying-type-based policy In the first case, the user specifies the range, and the library can choose the best underlying type for the job.
In the second case, the user chooses the underlying type, and this implies the range [numeric_limits<T>::min(), numeric_limits<T>::max()]
That seems like a reasonable idea, but what about the latter is "small" and not "safe" that you think the name should be "small_int"? Once you need two types to distinguish between a user-defined range, and a numeric_limits-defined range, why not just use two template names, like safe_range and safe_int? That would eliminate your p_range and p_type policies. However, since min() and max() are now constexpr, that all can be collapsed into a single template with three parameterizing types: the underlying type, the minimum, and the maximum: template < class T , T Min = std::numeric_limits<T>::min() , T Max = std::numeric_limits<T>::max()
class safe; ___ Rob (Sent from my portable computation engine)
2015-12-14 14:01 GMT+01:00 Rob Stewart
On December 14, 2015 3:43:25 AM EST, Andrzej Krzemienski < akrzemi1@gmail.com> wrote:
2015-12-12 1:18 GMT+01:00 Robert Ramey
: In the context of this library the safe_range ... are important for a very special reason. The bounds are carried around with type of expression results. So if I write
save<int> a, x, b, y; y = a * x + b;
runtime checking will generally have to be performed. But if I happen to know that my variables are limited to certain range.
safe_integer_range<-100, 100> a, x, b, y; y = a * x + b;
Then it can be known at compile time that y can never overflow so no runtime checking is required. Here we've achieved the holy grail:
a) guaranteed correct arithmetic result b) no runtime overhead. c) no exception code emitted. d) no special code - we just write algebraic expressions
This is the true motivation for safe_..._range
Why isn't that the behavior of your safe type in the first place? That is, what benefit does your safe type offer that it shouldn't just be supplanted by the range type?
Ok, now I think I understand the scope of the library. You give me the tool for representing a mathematical notion of an integral number within *some* limited range. The contract is:
You guarantee: 1. Either correct result or a compile time error or a run-time exception. 2. No memory management: you will only take the space of one scalar type.
I specify the range of the representable values. I can say: 1. "From -100 to +100", or 2. "Whatever range 'int' as on my machine".
But if I got it right, I would say that the choice of the name, and the interface do not indicate the intent as clear as they could. Imagine the following alternative interface:
small_int
>; // range-based policy small_int ; // underlying-type-based policy In the first case, the user specifies the range, and the library can choose the best underlying type for the job.
In the second case, the user chooses the underlying type, and this implies the range [numeric_limits<T>::min(), numeric_limits<T>::max()]
That seems like a reasonable idea, but what about the latter is "small" and not "safe" that you think the name should be "small_int"?
You have "big_integer" (or "integer") to represent a mathematical notion of integer that allocates memory. Analogously, you would have "small_integer" that represents a mathematical notion of integer with a reduced range of values. "safe_int" does not convey the idea of a higher level abstraction: it sounds like "an ordinary low-level 'int' with some modified rules".
Once you need two types to distinguish between a user-defined range, and a numeric_limits-defined range, why not just use two template names, like safe_range and safe_int? That would eliminate your p_range and p_type policies.
However, since min() and max() are now constexpr, that all can be collapsed into a single template with three parameterizing types: the underlying type, the minimum, and the maximum:
template < class T , T Min = std::numeric_limits<T>::min() , T Max = std::numeric_limits<T>::max()
class safe;
Current implementation has more of these policies (e.g., for how you want to report overflow). Regards, &rzej
On 12/14/15 5:15 AM, Andrzej Krzemienski wrote:
Then it can be known at compile time that y can never overflow so no runtime checking is required. Here we've achieved the holy grail:
a) guaranteed correct arithmetic result b) no runtime overhead. c) no exception code emitted. d) no special code - we just write algebraic expressions
This is the true motivation for safe_..._range
Why isn't that the behavior of your safe type in the first place?
it is That is,
what benefit does your safe type offer that it shouldn't just be supplanted by the range type?
safe<T> can be used as a drop in replacement for T . safe...range cannot.
However, since min() and max() are now constexpr, that all can be collapsed into a single template with three parameterizing types: the underlying type, the minimum, and the maximum:
template < class T , T Min = std::numeric_limits<T>::min() , T Max = std::numeric_limits<T>::max()
class safe;
That's exactly what safe<T> is.
Current implementation has more of these policies (e.g., for how you want to report overflow).
Is there a question in there somewhere?
2015-12-14 17:39 GMT+01:00 Robert Ramey
That is,
what benefit does your safe type offer that it shouldn't just be
supplanted by the range type?
safe<T> can be used as a drop in replacement for T . safe...range cannot.
Ok, I see. So, there is something more to it than just the set of values. safe<T> does not check upon conversion from T.
However, since min() and max() are now constexpr, that all can be
collapsed into a single template with three parameterizing types: the underlying type, the minimum, and the maximum:
template < class T , T Min = std::numeric_limits<T>::min() , T Max = std::numeric_limits<T>::max()
class safe;
That's exactly what safe<T> is.
Current implementation has more of these policies (e.g., for how you want
to report overflow).
Is there a question in there somewhere?
Nope: I was just replying to Rob.
2015-12-14 18:34 GMT+01:00 Robert Ramey
On 12/14/15 9:21 AM, Andrzej Krzemienski wrote:
2015-12-14 17:39 GMT+01:00 Robert Ramey
: Ok, I see. So, there is something more to it than just the set of values.
safe<T> does not check upon conversion from T.
I believe it does. If it doesn't it's a mistake.
It would make sense to skip the check: int i = whatever(); safe<int> si (i); // this just cannot go wrong.
On 12/14/15 9:45 AM, Andrzej Krzemienski wrote:
2015-12-14 18:34 GMT+01:00 Robert Ramey
: On 12/14/15 9:21 AM, Andrzej Krzemienski wrote:
2015-12-14 17:39 GMT+01:00 Robert Ramey
: Ok, I see. So, there is something more to it than just the set of values.
safe<T> does not check upon conversion from T.
I believe it does. If it doesn't it's a mistake.
It would make sense to skip the check:
int i = whatever(); safe<int> si (i); // this just cannot go wrong.
It does that. I guess I misunderstood the comment. Basically given a binary operation R <- T op U where either T or U are safe types, the operation is checked at runtime if and only if a) either T or U is a safe type b) the result cannot be guaranteed to fit into a type R Robert Ramey
On December 14, 2015 11:39:50 AM EST, Robert Ramey
On 12/14/15 5:15 AM, Andrzej Krzemienski wrote:
Most of what you quoted/replied to was from me, not Andrzej.
Then it can be known at compile time that y can never overflow so no runtime checking is required. Here we've achieved the holy grail:
a) guaranteed correct arithmetic result b) no runtime overhead. c) no exception code emitted. d) no special code - we just write algebraic expressions
This is the true motivation for safe_..._range
Why isn't that the behavior of your safe type in the first place?
it is
I'm confused. If that's the behavior of safe, why do you need safe_..._range?
That is,
what benefit does your safe type offer that it shouldn't just be supplanted by the range type?
safe<T> can be used as a drop in replacement for T . safe...range cannot.
Why not?
However, since min() and max() are now constexpr, that all can be collapsed into a single template with three parameterizing types: the underlying type, the minimum, and the maximum:
template < class T , T Min = std::numeric_limits<T>::min() , T Max = std::numeric_limits<T>::max()
class safe;
That's exactly what safe<T> is.
Why doesn't that suffice for all use cases? ___ Rob (Sent from my portable computation engine)
On 12/14/15 11:15 AM, Rob Stewart wrote:
On December 14, 2015 11:39:50 AM EST, Robert Ramey
wrote: runtime checking is required. Here we've achieved the holy grail:
a) guaranteed correct arithmetic result b) no runtime overhead. c) no exception code emitted. d) no special code - we just write algebraic expressions
This is the true motivation for safe_..._range
Why isn't that the behavior of your safe type in the first place?
it is
I'm confused. If that's the behavior of safe, why do you need safe_..._range?
That is,
what benefit does your safe type offer that it shouldn't just be supplanted by the range type?
safe<T> can be used as a drop in replacement for T . safe...range cannot.
Why not?
It's really syntactical sugar everywhere I int I can now use safe<int>. Without this, I'd have to write safe_integer_range< std::numeric_limits<int>::min(), std::numeric_limits<int>::max()
which would become tedious.
However, since min() and max() are now constexpr, that all can be collapsed into a single template with three parameterizing types: the underlying type, the minimum, and the maximum:
template < class T , T Min = std::numeric_limits<T>::min() , T Max = std::numeric_limits<T>::max()
class safe;
That's exactly what safe<T> is.
Why doesn't that suffice for all use cases?
Hmm - can I rephrase this question as
what is safe_range
On December 14, 2015 3:23:28 PM EST, Robert Ramey
On 12/14/15 11:15 AM, Rob Stewart wrote:
On December 14, 2015 11:39:50 AM EST, Robert Ramey
wrote: > runtime checking is required. Here we've achieved the holy grail: > > a) guaranteed correct arithmetic result > b) no runtime overhead. > c) no exception code emitted. > d) no special code - we just write algebraic expressions > > This is the true motivation for safe_..._range
Why isn't that the behavior of your safe type in the first place?
it is
I'm confused. If that's the behavior of safe, why do you need safe_..._range?
That is,
what benefit does your safe type offer that it shouldn't just be supplanted by the range type?
safe<T> can be used as a drop in replacement for T . safe...range cannot.
Why not?
It's really syntactical sugar
everywhere I int I can now use safe<int>. Without this, I'd have to write
safe_integer_range< std::numeric_limits<int>::min(), std::numeric_limits<int>::max()
which would become tedious.
I showed a safe class template, below, that defaulted the min and max to those values, and you said that was "exactly what safe<T> is". If yours has those defaults, then there is no such tedium.
However, since min() and max() are now constexpr, that all can be collapsed into a single template with three parameterizing types: the underlying type, the minimum, and the maximum:
template < class T , T Min = std::numeric_limits<T>::min() , T Max = std::numeric_limits<T>::max()
class safe;
That's exactly what safe<T> is.
Why doesn't that suffice for all use cases?
Hmm - can I rephrase this question as
what is safe_range
good for? answer - lot's of things
b) The main driver is a case like the following:
int a, x int y = a * x; // possible error
safe<int> a, x; safe<int> y = a * x; // possible error - but checked at runtime
safe_integer_range<-10, 10> a, x; safe<int> y = a * x; // no runtime checking necessary
though of course something like the following would be useful
using minutes_t = safe_unsigned_range<0, 59>
There are other examples in the documentation.
I'd expect that from the one safe class template. I'm missing something. ___ Rob (Sent from my portable computation engine)
On 12/14/15 1:01 PM, Rob Stewart wrote:
I showed a safe class template, below, that defaulted the min and max to those values, and you said that was "exactly what safe<T> is". If yours has those defaults, then there is no such tedium.
However, since min() and max() are now constexpr, that all can be collapsed into a single template with three parameterizing types: the underlying type, the minimum, and the maximum:
template < class T , T Min = std::numeric_limits<T>::min() , T Max = std::numeric_limits<T>::max() > class safe;
That's exactly what safe<T> is.
Why doesn't that suffice for all use cases?
Whoops, I read the above too fast. The reason is the we need to be able to specify policy classes for exception handling and type promotion. The actual template is template< class Stored, Stored Min, Stored Max, class P, // promotion polic class E // exception policy
safe_base; then we have template < class T, class P = native, class E = throw_exception
using safe = safe_base< T, std::numeric_limits<T>::min(), std::numeric_limits<T>::max(), P, E
;
and template < std::intmax_t MIN, std::intmax_t MAX, class P = native, class E = throw_exception
using safe_signed_range = safe_base<
typename detail::signed_stored_type
;
So it's really just a question of getting the default parameters correct for different usages. Robert Ramey
On 12/14/15 12:43 AM, Andrzej Krzemienski wrote:
Ok, now I think I understand the scope of the library. You give me the tool for representing a mathematical notion of an integral number within *some* limited range. The contract is:
You guarantee: 1. Either correct result or a compile time error or a run-time exception. 2. No memory management: you will only take the space of one scalar type.
I would (and have) phrase it differently. 1. Either correct result or a compile time error or a run-time exception. 2. Best runtime performance possible subject to 1. above. So "No memory management" isn't a fundamental principle, but rather implied by 2. above in our current machines.
I specify the range of the representable values. I can say: 1. "From -100 to +100", or 2. "Whatever range 'int' as on my machine".
If you look at the code you'll see something like
template ; But if I got it right, I would say that the choice of the name, and the
interface do not indicate the intent as clear as they could. Imagine the
following alternative interface: small_int I call this safe_integer_range<-100, 100> - I don't think that's
significantly different. small_int I'm not sure what this is meant to mean. But note than many of my tests
and examples I often use something like the following to make this clearer
template
using short_int_t = safe_range<0, 32>; In the first case, the user specifies the range, and the library can choose
the best underlying type for the job. that's what safe_... range does. In the second case, the user chooses the underlying type, and this implies
the range [numeric_limits<T>::min(), numeric_limits<T>::max()] that's what safe<T> does.
Robert Ramey
participants (6)
-
Andrzej Krzemienski
-
Damian Vicino
-
Oliver Kullmann
-
Pete Bartlett
-
Rob Stewart
-
Robert Ramey