At this point, when I see libraries like this all I can think is ”oh good, my compile times aren’t long enough, lets make EVERY INTEGER a template”.
Also, while I can reasonably beliveve that most of the overhead goes away at -O2 or -O3, this has to just trash performance for debug builds, which is not unimportant.
It does not change your existing integers or compilation time. Only where you use the new types the compiler will have more work to do. What would be the alternative in those places? Manual code? Preprocessor macros? Assertions everywhere? A theorem prover?
To me the implementation seems to be a good solution given that you don't want to change the core language.
Stop worrying about 9-bit bytes, only logical shift right for signed integers, ones-complement and sign-bit integers, etc and define this stuff in the standard. PDP-11 was a long time ago. C++20 is heading in the right direction with defining twos-complement. The next good step would be declaring signed overflow as wrapping (eg -fwrapv)
And if the application's run-time safety requirements demand that the debug performance hit be paid, then that audience will view the debug sluggishness as a cost of doing business.
Library author here: As I have added more features to the library, compile times have definitely suffered. Fortunately, the latest version of the clang compiler added `-ftime-trace`, which reports where the compiler spends its time. I am currently going through and optimizing the compile-time performance of my library. However, I expect the largest gain to come in the next year or so once compilers + build systems implement C++20 modules so that you don't have to compile everything every time you use it.
For run-time performance, yes, you definitely get a hit in your run time if you have all optimizations off. However, I have found that the practical benefits of things like this library, combined with run-time sanitizers, is worth much more at finding bugs than a debugger, so I typically debug and develop with `-O3 -fsanitize=undefined -fsanitize=address` and release mode is `-O3 -flto=thin -DNDEBUG`. This is an area where it's not possible (yet) to satisfy all use cases. I am hopeful that in the future, it will be much easier to tell your compiler "this is the code I care about debugging, optimize the rest".
In my line of software work, I needed variable-precision integers (think uint5_t, uint13_t, etc) to express details of the architecture cleanly and avoid lots of manual bit-masking, which naturally meant having to use templates.
I found that it didn't really hurt compile times much (500KLOC compiles in about 40 seconds with make -j on a 16-core box), and performance is just barely impacted at -O2, but it definitely destroys debug build performance completely and makes a mess of code profiling and backtrace debugging output.
There are also all kinds of subtle gotchas: you can make "x ? y : z" work when y and z are different primitive types, but not if one is a template. You cannot make them work with printf("%d", x) directly (it compiles fine and then you run into problems), etc.
Trying to enforce stronger type checking (not allowing the implicit mixing of signed and unsigned, etc) introduces endless worlds of pain and ambiguous operator overload problems.
Trying to make eg "x++" or "x+y" return another template instance instead of a primitive type similarly causes a world of pain everywhere.
I still feel it was 100% worth it in my case, but it has many pitfalls. Taking it even further to bounding to specific ranges as this library aims to do feels even more fraught with danger. I do not envy the work of its authors.
> You cannot make them work with printf("%d", x) directly (it compiles fine and then you run into problems), etc.
To be fair that's also true for standard intN_t or uintN_t types. "%d" expects an int argument, and while several of the intN_t types may be converted to int by the default argument type promotion rules, that isn't guaranteed. For example, while plain "%d" works for int64_t on ILP64 platforms with 64-bit int values, LP64 (32-bit int, 64-bit long) requires "%ld" and 32-bit platforms require "%lld". That's why you have e.g. the PRId64 macro from <inttypes.h> to select the correct format code for printing int64_t values. You could provide something similar to abstract away the required format code. If you're using the GNU C library you can even define custom format codes with register_printf_function(), which might be your only option if the calling convention for your template doesn't match any of the standard integer types.
That's a variation of "did you even read the article", which the HN guidelines specifically ask users not to post, so please edit those out of your posts. The comment would be just fine without that bit.
Of course it will be impacted. Libraries like these are "zero-cost" only because of very aggressive inlining (not generally available in debug builds), otherwise all its operations happen in subroutines. Operator overloading is the most obvious example: for regular integers, addition is basically a single instruction even in debug, but with a library like this, it has to make a subroutine call. A call which has who knows how many layers of abstraction that compiler optimizations hasn't shaken off.
In fact, the README says as much: it has only zero time/space overhead "assuming basic compiler optimizations like inlining".
And I did actually look at the code, but at a glance I couldn't get much sense of it. The main header imports like two dozen other headers, and the "detail" folder is filled to the brim with more headers. I tried cloning and using it, but I couldn't get it to compile (which is probably mostly my fault, but I really didn't want to spend too much time on it). I did run just the preprocessor though, and "#include <bounded/integer.hpp>" expanded to 48000 lines. 48000 extra lines to compile in order to use integers. For every file you use it in.
I don't want to be too harsh here, I'm sure it's an excellent library and it does what it says brilliantly, and if you need bounded integers I'm sure it's awesome. But language like "[integers in C++] are mostly unusable" rankles a bit, and the the implication in this file is that this is something you should regularly use instead of integer types.
I personally feel that libraries like this are taking C++ in the wrong direction ("ranges" is another obvious example) and making the language less and less usable in my profession.
> but with a library like this, it has to make a subroutine call.
I mean, before I wrote my comment I checked and it's `constexpr` all the way down to the add instruction so if it's going to make a call, I'm not seeing it. There is definitely a lot of template machinery, but I can't be the judge of that immediately.
> I personally feel that libraries like this are taking C++ in the wrong direction ("ranges" is another obvious example)
I think you're making an emotional argument based on some preconceived twitter-verse sentiment. I'm in games too (graphics specifically) and people are up and arms about the wrong thing. Maybe there are corner cases that are a bit overly complex, but what's wrong with being able to write `std::sort(container)` instead of `std::sort(container.begin, container.end)`. There may be things we don't like, but I don't think we should resort to hyperbole either.
Incidentally, I wouldn't use this library if only because for what I do, having explicitly sized data types is important.
constexpr functions are not inlined in debug builds. They can't be: the whole point of debug builds is that you can attach a debugger and step through the code.
Right - boost::units is supposed to be a zero-overhead abstraction buy there are still edge cases where the compiler can’t optimize away all the template wrappings. Generally still worth it, if you find a performance bottleneck you can move back to doubles and conventions to keep the units straight. This sounds like it may not be so easy to selectively opt out of.
High level languages are sold on the concept that customers' machines are powerful enough that the reduction in performance is worth it for the reduction in development time. For the same reason, it should be worthwhile to give developers powerful machines so they spend less time fixing runtime bugs.
Given that many of the security features did run perfectly fine in 60's hardware, I think they can manage in 21st century machines.
"Many years later we asked our customers whether they wished us to provide an option to switch off these checks in the interests of efficiency on production runs. Unanimously, they urged us not to--they already knew how frequently subscript errors occur on production runs where failure to detect them could be disastrous. I note with fear and horror that even in 1980, language designers and users have not learned this lesson. In any respectable branch of engineering, failure to observe such elementary precautions would have long been against the law."
-- Tony Hoare, "The 1980 ACM Turing Award Lecture"
Yes, that’s what I mean. And before you say ”debug performance doesn’t matter”: yes it does. I work in gamedev and it’s a huge problem that C++ has such awful debug performance, because it’s hard to debug a game if it’s running in single-digit framerates. It’s a large part of the reason why EASTL is so popular in gamedev.
I feel like that's mostly Microsoft's fault, their STL implementation is super aggressive about checks in debug builds to the point that it becomes sort of useless in heavy use/high performance scenarios. Between that and the lack of valgrind, I'm pretty glad I left that world.
Over in Linux/g++ land, it's not nearly so bad. Bounds errors are mostly handled by address sanitizer such that -O0 performance is decent. And if -O0 is too slow, there's -Og which takes slightly longer to compile and has less information for gdb.
I work almost exclusively in "g++ -O0" until it's time to release.
Clang is decent too, but I find the C++ compile times are atrocious, like almost double for my admittedly template-heavy code. And performance of the compiled output is no better.
In more explicit terms-- you are a soft-realtime app developer trying to live in a world that rarely measures performance or even writes specs with your constraints in mind.
I wish you game devs would harp on this more. I'm just a lowly soft-realtime audio software dev. My voice doesn't carry because there aren't mountains of cash behind me to help echo it.
oh it's been harped on for years. But people with mountains of cash already wrote their own STL and heap allocators :-) Those are usually your main bottlenecks in usability of debug builds.
I've worked on games with "DebugOptimized" builds where it's optimized,but instrumented in other ways. That's probably the best solution. You only drop down to full debug if you absolutely can't debug a specific issue in an optimized build.
With Visual C++, if you are careful you can build a version with release runtimes and third party libs so only your code is debug. Microsoft makes it harder than it has to be though.
We absolutely do, but it’s significantly harder to debug. Functions are inlined, variables are eliminated, loops unrolled, etc.
But yeah, very often it’s basically the only option, running an optimized build with debug symbols. This is why it’s such a problem with C++: debug builds are frequently pointless because the performance is so bad, which makes debugging a lot harder.
Yeah that makes sense. There's no good alternative in C++, thanks to the mess created by all the zero-cost abstractions that are very much not zero-cost without a the optimizations of a release build.
Single-level inlining helps a lot with that but any libraries that do more calls that should be inlined are still a pain (and it reduces your debugging ability with, say, lambdas in VS at least).
Oh, that's nice. I wanted that decades ago, when I was working on program verification. Templates have made a lot of progress if this can be done entirely in C++ templates. I once wrote, but never published, "Type Integer Considered Harmful", back when there were still 16-bit integers in most C programs. I wanted ranges on everything, like Ada. As a practical matter, integer overflow became less of an issue with 32-bit.
Sizes of intermediates are a big issue. When you write
int_32 a,b,c,n;
...
n = (a*b)*c;
how big is each part? My thinking on this was that it's the compiler's job to prevent overflow in intermediate values where the final result will not overflow. So, above, you'd have to compute (m * n) as a 64-bit product, do a 64-bit divide, and only then check that the result fit in n.
is legal to compute in 32-bit, but requires overflow checking on the intermediates. If an overflow occurs, there will be an overflow in the result. (Although, the case where some values are zero is an issue. Suppose a * b overflows but c is zero so it doesn't matter. That's probably an error.)
Sometimes you have to use larger sized intermediates. For
int_32 m,n,p;
...
n = (m * n) / p;
how big is each part? Above, you'd have to compute (m * n) as a 64-bit product, do a 64-bit divide, and only then check that the result fit in n.
To do this right, you need something in the compiler that can do basic reasoning about machine arithmetic. Something that knows, for example, that
uint_16 n;
...
n = (n + 1) % 65536;
cannot really overflow and can be optimized down to a plain unsigned 16-bit add.
If you try to to this through linguistic type analysis only, it's not going to be satisfactory.
You need to be able to prove out inequalities.
Sizes of intermediates are a big issue.
My thinking on this was that it's the compiler's job to prevent overflow in intermediate values where the final result will not overflow.
When you write
int_32 a,b,c,n;
...
n = (a*b)*c;
that's legal to compute in 32-bit, but requires overflow checking on the intermediates. If an overflow occurs, there will be an overflow in the result. (Although, the case where some values are zero is an issue. Suppose a * b overflows but c is zero so it doesn't matter. That's probably an error.)
Sometimes you have to use larger sized intermediates. For
int_32 m,n,p;
...
n = (m * n) / p;
how big is each part? Above, you'd have to compute (m * n) as a 64-bit product, do a 64-bit divide, and only then check that the result fit in n.
By all means be as imprecise and incorrect as you want, but you can hardly complain when people on a technical forum correct your imprecision ¯\_(ツ)_/¯
What, precisely, did you find rude about “ Ada. It’s not an acronym. ADA is the Americans with Disabilities Act.”?
Sincere question. Rudeness was not my intention, and those three sentences appear to be three simple facts completely neutral in nature. I’m not understanding how they could possibly be perceived as rude.
“go away” on the other hand, seemed an unnecessarily hostile and rude response to such a neutral correction, and I think the voting well reflects that. On the whole I get the feeling I’d prefer not interacting with someone so hostile about nothing, too, but I’m hardly inclined to let the assertion that I was rude go unchallenged.
What part of ”I’m disinclined to let ‘you were rude to me‘ go unchallenged just because you followed it up with ‘stop interacting with me’” do you not get?
You don’t get to accuse people of things and then summarily declare that they’re not allowed to defend themselves against your daft accusations.
This is a forum. If you don’t want people responding to you, don’t post. You don’t get to play silly little games like “You believe the earth is flat and also I wave my magic wand and forbid you from replying to me, so that claim cannot be challenged”. That’s not how a discussion forum works.
Or Lisp’s approach of just using ”big” ints by default (which is practically the same but with an implicit default of “infinity”). And trust the compiler to use fixints internally when the programmer has declared it safe to do so.
I just finished writing a custom FIFO allocator for an extremely high bandwidth and low latency data processing (and UI) system written in C/C++ (even std::deque was doing WAY too many heap allocations, not to mention the allocations within each object passing through the system, despite use of move semantics to minimize redundancy).
Performance improved by 100x - 1000x. And it was already blazingly fast before, if measured against performance standards we’ve become accustomed to from JavaScript and other GC languages.
In high performance systems (where the benefits of C/C++/etc. outweigh the downsides), dynamic allocations always come back to bite you.
If you rely on them too heavily from the start (and don’t plan for custom allocation schemes in the future), you can even get into bad situations where it’s infeasible to refactor to custom memory management (without a total rewrite), when you later need the performance gain.
Incorporating even the possibility of heap allocations into a language’s most fundamental data types will doom that language to being relegated to performance-insensitive and latency-insensitive tasks, if only because it requires that a heap exist (whereas C, Rust, etc can run on embedded real-time systems with no heap).
And that’s okay! It’s good that we have languages for that. But C/C++/Rust/etc. are definitely not where you can tolerate such a thing in the core language.
No. Thread local/core-specific FIFO is more cache efficient than a ring buffer because the address about to be allocated is significantly more likely to be in a high level cache.
With a ring buffer, you're constantly cycling out to L3 or worse and hoping the prefetcher figures out what you intend. It's basically a LIFO allocator.
Even if you want to use a separate processing core, you get better latency using a FIFO allocator between 2 threads on the same core complex and the code is simpler, reducing instruction fetch overhead.
Frankly, I see architectures like yours all the time from firms like Hudson River Trading and I think they suck. They incur tons of overhead, the process separation adds a useless layer of abstraction that's annoying to transcend, and you end up with this useless message protocol between cores that invokes tons of copies and breaks compiler inlining features.
Ring buffers have a strictly sequential access pattern, which prefetchers are specifically optimized for.
Anybody using a "message protocol" with ring buffers, or doing copies out of them, is Doing It Wrong. I routinely get 10x performance by doing away with FIFOs and buffer allocation and freeing.
Process separation means you can start and stop readers independently of any other activity.
When I see you say “any brand of FIFO loses” in the same post as “what you need is a ring buffer”, it shows that there’s a terminology disconnect here: A “ring buffer” used for streaming data is a brand of ”FIFO” :)
Therefore, the implementation you’re suggesting is actually not all that different from my current solution, except for a few important details related to the particular problem I’m solving — e.g. handling many parallel streams which may momentarily drift out of sync (where those that are not delayed must still be processed without the state of other streams interfering to add latency), among other important details.
Ultimately though, aside from this discussion on high performance designs (which though fun, would not work without you actually knowing the requirements of what I’m working on — e.g. it’s not HFT), I’m just glad we’re in agreement that there are applications where avoiding dynamic allocations is absolutely essential, and that it would be a huge mistake to add them to a high-performance language’s most fundamental integer types.
That is an interesting problem. When I see it, the input streams tend to be ring buffers, and output is a priority queue of their heads, ordered by timestamp.
Obviously a ring buffer is, literally, a first-in first-out medium. The essential difference between your typical FIFO and a ring buffer is the entire lack of interaction between writer and reader. More precisely, readers never have any effect on the writer. This allows any number of readers to be at random places along the sequence. The FIFO queues I find myself replacing tend to allocate a buffer, under a lock, and push it to a queue, under another lock (often these are hardware locks, what is often called "lock-free"), and readers pop a buffer under the same lock, use them for awhile, and then return them to a pool, under ther first lock. All this interaction generates overhead and cache pollution.
The only coupling in a ring buffer is readers checking the current state of the head pointer, done under relaxed semantics. The head pointer lives at all times in the writer's cache.
You probably understand all this, but remarkably many don't.
The performance hit when the system has to switch from regular integers to BigInts is likely to be unacceptable in the realtime / safety-critical applications where we really care about bounds guarantees. Consider an autopilot system where the execution time massively increases when some critical value exceeds some bounds, and the system can no longer execute in the time required. In floating point systems, this is often experienced when "denormalized" numbers are encountered.
In other words, it's not just guaranteed bounds on the value that is required, but bounded (and consistent) execution time.
> A smart compiler can narrow it down to machine size integers then possible.
A smart compiler will be able to narrow it down in a limited set of scenarios, but all we need to do is put an accumulator into a loop to see that bounding an integer is equivalent to the halting problem. Or for another example, should we expect our "smart" compiler to bound y in the following? The bound on x is a freebie.
bigint n(uint64_t x) {
bigint i = 0;
bigint y = x;
while (y != 1) {
i++;
if (y%2) y = y//2;
else y = 3*y-1;
}
return i;
}
As I said, "when possible". When not possible a smart compiler can give a warning or an error and ask the user to either use arbirary-precision integer or a type with bound checking.
In many situations yes, and C/C++ compilers miss a lot of opportunities for constant folding/propagation. But still, an infinitely smart compiler can’t prove anything about a value that comes from a file, user input or socket at runtime. If control flow depends on those (which is true of every useful real-life program) then you’re out of luck. GMP arbitrary-size integers are amazingly fast but still much slower than native instructions, can’t be vectorized, require heap allocation, etc.
It takes quite some amount of stamina to keep on reading after "The built-in integer types in C++ (int, unsigned, long long, etc.) are mostly unusable"
I can see why you'd say that that way but the author probably feels that way because of the domain they work in. I've encountered research types who just use bigint everywhere because it's safer and they don't worry about the perf.
When I was coming up as a game programmer no one would use boost shared_ptr and weak_ptr because of perceived issues with it. Everyone rolled their own handle system.
In fact there were a lot of people that considered languages beside C/C++ untouchable due to perf. They couldn't fathom there were domains where that perf just wasn't that important.
I now work a lot with Unity and find people feel the same about C# foreach because it used to trigger an allocation.
You'll find people often make the mistake of thinking their domain specific problem is a problem for everyone.
> bounded::integer uses built-in integers as the template parameter to determine its bounds. This means that it cannot store an integer larger than an unsigned 64-bit or unsigned 128-bit integer (depending on the platform) or smaller than a signed 64-bit or signed 128-bit integer. This restriction should be removed at some point in the future.
So when an overflow happens here I assume it's a compile time error (UB due to integer overflow in constexpr context must be diagnosed by the compiler). A handful of multiplications can get you there easily.
I am not aware of any compiler or platform where int is 128 bits. The most common these days, even on 64 bit platforms, is 32 bits, which they don't even mention.
I am not sure why they chose to speculate about sizeof(int) in this way, vs. expressing the limit in terms of something more concrete.
Library author here. I'm sorry you found the wording confusing. I was trying to talk generally about the non-library types: things like int and long. The C++ standard guarantees that there exists an integer type that is at least 64-bits large (`long long` and `unsigned long long`), and many platforms support a type like __int128 that is 128 bits large. Unfortunately, the documentation is slightly out of date. clang has some bugs that cause the compiler to crash if you use __int128 or __uint128 in certain situations (which my library uses), so I had 128-bit support on only in gcc. However, my library is currently making use of C++20 concepts, and only a branch on clang supports that enough, so the master version of my library can be compiled only by clang, so the largest size currently allowed is a 64-bit integer (either signed or unsigned, the library handles that for you).
I am saying the author did not express the limitation well, and instead made it sound like they don't understand sizeof(int). The explanation that leni536 offered makes a lot more sense, so maybe they should have put it that way instead of using an easily misunderstood term as "built-in integer" to mean "one of several integer types that we may choose based on some ifdef"
> bounded::integer uses built-in integers as the template parameter to determine its bounds. This means that it cannot store an integer larger than ...
They say the limitation arises from using "built-in integers" as template parameters. It is reasonable to assume "built-in integer" means "int". It is more a stretch to assume "built-in integer" means any number of types depending on the evaluation of several ifdefs [which is what I found in the source].
There is nothing about a "use case" relevant to any of that quoted statement, so I find your reply very confusing too.
> It is reasonable to assume "built-in integer" means "int".
This strikes me as a little nuts for C but it definitely explains this thread. I just assumed people had already switched over to e.g. sint32/size_t to avoid this problem a long time ago. I couldn’t tell if this was a legitimate complaint or someone’s language lawyering tendencies gone too far.
First of all, we're not talking C. This is a C++ template parameter. The feature doesn't exist in C.
Second, it is not "nuts" to use int, I would argue it's quite a bit more crazy to use rarely-supported 128-bit quantities when you don't need them. Your suggestion of "sint32" (this is not a standardized typedef, you mean int32_t perhaps) would be the same as int on most compilers today, and size_t is likewise very often 32 bits. Your suggestion is basically a no-op in a lot of places. Domain-specific areas like file formats or network protocols are a different story, as those have to be explicit about specifying sizes.
Third:
> someone’s language lawyering tendencies
I suggest you avoid passive-aggressive communication style and just say "you" instead of "someone". If you're going to criticize or give feedback, be direct. Don't vaguely tell me that "somebody" "might" have a problem.
> The built-in integer types in C++ (int, unsigned, long long, etc.) are mostly unusable
Weird.
Ive been programming in C/C++ professionally for over a decade and have been using built-in integer types mostly without issue.
Might want to qualify your hyperbole- is it worth reading on?
I agree with that statement. How can you use type when you don't know range of values. I want to store year. From 1850 to 2050. What type should I choose? I want to store colour value: from 0 to 2^24-1. What type should I choose?
How long does it take you to code a program if you are being bogged down by such simple problems? Use ints and do bounds checking when it's necessary to do so. Is every single bucket of information a reinvention of types?
It specifies the minimum sizes, in the sense that sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) etc... but no requirements on how big they actually are. You could (in theory) end up with all of those being 9 bits wide on some weird platform.
> Worse, the compiler is entitled to use saturation arithmetic if you've assumed wrapping.
C89 (draft[1], because the actual spec is not public) section 3.1.2.5 states:
> ... a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented by the resulting unsigned integer type.
So, unless I'm misunderstanding, the compiler is not entitled to use saturation arithmetic on unsigned integers.
You should never rely on the underlying CPU architecture's behavior on integer overflow when you write C or C++. It is undefined and not unspecified or implementation defined.
Undefined behaviour is undefined only by the standard - an implementation may choose to handle undefined behaviour in some documented, specific fashion. Undefined behaviour exists to give implementations exactly this flexibility.
true, but the behavior of unsigned integers is defined such that they can't overflow. the "wrapping" behavior of unsigned ints is defined: you get the arithmetic result modulo the max value for that width.
> I agree with that statement. How can you use type when you don't know range of values.
That might be a question for developers of the billons of lines of C++ in production. Asking them, you'd discover how they they actually use the builtin types.
If you work with smaller embedded systems and you need portability this is also a common sight (when you also quote the part about cstdint, which most embedded people will be happy with).
On a different platform, these "friendly" types can be larger, typically 2 or 4 times larger which can be devastating for performance, or smaller, leading to overflow. They are only "usable" if you restrict yourself on one version, of one compiler, on one hardware platform.
An unsigned type is sometimes a poor choice for a loop index. The compiler then cannot assume the loop will terminate, and generate worse code. Usually this doesn't matter -- until you discover it did.
Good point. Probably not a problem in practice for most containers, but yet another reason for why the STL (and C library, for that matter) should have used a signed size type from the beginning.
You can still use the Uint32/Uint8/... types. While performance can vary from system to system, the range should be the same on all reasonable platforms.
Because of the field I work in I feel this much more keenly with floats. I have been bitten by their excentricities so many times I'd just much rather have a flexible fixed point type that let me choose how much precision I want on either side of the decimal for a given set of calculations.
Both libraries do the same thing. Boost lets you set your exception policy and I'm unsure if this Bounded Integer library gives the same level of control.
The bounded::integer types accept three template parameters: `integer<min, max, overflow_policy>`. That final parameter can be bounded::throw_policy, which is itself templated on the exception type thrown. The default exception policy is "overflow is undefined behavior". The other two policies supported out of the box are wrapping / modulo and saturation / clamping.
> throwing an exception on overflow or clamping the value to the minimum or maximum are also possible by use of template policies (and those particular use cases are already built in to the library)
> Never perform a run-time check when a static check would work instead
... but sometimes do run-time checks?
> Have no space or time overhead
This is a very confusing set of claims. You can't have all of these at the same time. Dynamic checks will be needed more often than you'd think, even in cases that "we" can tell won't overflow, so you'll definitely pay for stuff that you use even though with a stronger system you wouldn't need to use it.
For example, I'm wondering if this system can eliminate dynamic checks in cases like these:
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
After this loop sum's type should be integer<55, 55>, but I'd bet (without having tried) that with this actual system it's inferred to integer<0, infinity> and you'd get dynamic checks. Unless you use the "null policy", in which case you don't need this library at all.
There is definitely a case to be made for a system that tries to trap overflows but also tries to use static analysis to be smart about where to put the dynamic checks. But I don't think this system is that.
Only very old versions of the library are supported by those older compilers. My library is targeting the C++20 standard, so currently the only compile version that can compile it is clang with the concepts branch.
Also, while I can reasonably beliveve that most of the overhead goes away at -O2 or -O3, this has to just trash performance for debug builds, which is not unimportant.