在chrono中处理溢出
It is well known that it is easy for <chrono> to silently overflow under the hood when pushing the boundaries of its range.
众所周知,当把它的取值范围推到边界时,<chrono> 很容易在底层悄悄溢出。
For example:
using namespace std::chrono;
years y{300};
nanoseconds x{300};
std::cout << (y > x) << '\n';
The output is typically
0
though this is actually undefined behavior. The output falsely indicates that 300ns is longer than 300 years.
例如:
using namespace std::chrono;
years y{300};
nanoseconds x{300};
std::cout << (y > x) << '\n';
输出通常是
0
尽管这实际上属于未定义行为。该输出错误地表示300纳秒比300年还长。
This happens because to do the comparison <chrono> first attempts to convert 300 years to nanoseconds, which overflows the underlying 64 bit signed integer used to represent 300 years worth of nanoseconds.
这是因为在进行比较 <chrono> 时,先尝试将300年转换为纳秒,这会导致用于表示300年纳秒数的底层64位带符号整数溢出。
The usual advice is to manually ensure that your chrono arithmetic does not approach overflow boundaries such as the nanoseconds +/- 292 year range.
通常的建议是手动确保你的chrono计算不会接近溢出边界,例如纳秒范围在+/- 292年之间。
Is there a way to easily automate this checking in modern C++, at least for debug builds?
是否有办法在现代C++中轻松自动化地进行这种检查,至少在调试构建中?
解决方案
Is there a way to easily automate this checking in modern C++, at least for debug builds?
Yes.
是的。
The <chrono> durations (and time_points) can use class types for the underlying representation as opposed to integral (or floating point) types. For example, they can use a "safe int" type that turns overflow into a noisy run-time error.
<chrono> durations(以及 time_points)可以使用类类型作为底层表示,而不是整型(或浮点型)。例如,它们可以使用一种“安全整型”类型,将溢出转化为一个嘈杂的运行时错误。
One free, open-source, header-only example of such a class is my bbi integer library.1 This library can be used to easily define your own custom chrono types that will terminate on overflow.
一个自由、开源、头文件独立的示例类是我开发的 bbi整数库。1这个库可以用来轻松定义你自己的自定义chrono类型,并在溢出时会 terminate。
For example:
#include "bbi.h"
#include <chrono>
#include <iostream>
namespace mychrono
{
using namespace bbi::term;
using nanoseconds = std::chrono::duration<i64, std::nano>;
using microseconds = std::chrono::duration<i64, std::micro>;
using milliseconds = std::chrono::duration<i64, std::milli>;
using seconds = std::chrono::duration<i64>;
using minutes = std::chrono::duration<i32, std::ratio<60>>;
using hours = std::chrono::duration<i32, std::ratio<3'600>>;
using days = std::chrono::duration<i32, std::ratio<86'400>>;
using weeks = std::chrono::duration<i32, std::ratio<604'800>>;
using months = std::chrono::duration<i32, std::ratio<2'629'746>>;
using years = std::chrono::duration<i32, std::ratio<31'556'952>>;
} // namespace mychrono
int
main()
{
using namespace mychrono;
years y{300};
nanoseconds x{300};
std::cout << (y > x) << '\n';
}
This requires C++20 or later, and on recent versions of MSVC, gcc and clang, will output the message:
Z<Signed, 64, Terminate>{300) * Z<Signed, 64, Terminate>{31556952000000000) overflowed
and then call std::terminate.
例如:
#include "bbi.h"
#include <chrono>
#include <iostream>
namespace mychrono
{
using namespace bbi::term;
using nanoseconds = std::chrono::duration<i64, std::nano>;
using microseconds = std::chrono::duration<i64, std::micro>;
using milliseconds = std::chrono::duration<i64, std::milli>;
using seconds = std::chrono::duration<i64>;
using minutes = std::chrono::duration<i32, std::ratio<60>>;
using hours = std::chrono::duration<i32, std::ratio<3'600>>;
using days = std::chrono::duration<i32, std::ratio<86'400>>;
using weeks = std::chrono::duration<i32, std::ratio<604'800>>;
using months = std::chrono::duration<i32, std::ratio<2'629'746>>;
using years = std::chrono::duration<i32, std::ratio<31'556'952>>;
} // namespace mychrono
int
main()
{
using namespace mychrono;
years y{300};
nanoseconds x{300};
std::cout << (y > x) << '\n';
}
这需要C++20或更高版本,在MSVC、GCC和 Clang的较新版本上,会输出以下信息:
Z<Signed, 64, Terminate>{300) * Z<Signed, 64, Terminate>{31556952000000000) overflowed
然后调用 std::terminate。
Working demo for all three major platforms.
Working demo 可用于所有三大平台。
The only changes to the code are:
对代码的唯一修改是:
#include "bbi.h"- Type aliases for the chrono types under your own namespace
-
Change
using namespace std::chronotousing namespace mychrono(or manage the namespace change however you prefer). -
#include "bbi.h" - 在你自己的命名空间下为chrono类型定义的类型别名
- 将
using namespace std::chrono修改为using namespace mychrono(或按你偏好的方式管理命名空间的变更)
Once you have debugged your code and want to return to the full efficiency of a release build, simply change:
一旦完成调试,想要回到发布构建的全性能,只需将:
using namespace bbi::term;
to:
改为:
using namespace bbi::raw;
This change redefines your custom chrono types to use built-in signed integers of the indicated bit width for their representation.
这一改动将重新定义你的自定义chrono类型,使其使用所指定位宽的内置带符号整数来表示。
If you would like to throw an exception on overflow, instead of call std::terminate, then change:
如果你希望在溢出时抛出异常,而不是调用 std::terminate,请将:
using namespace bbi::term;
to:
改为:
using namespace bbi::thrw;
Then, on overflow a std::overflow_error will be thrown with a what() message identical to the one printed out previously with terminate().
然后,在溢出时将抛出一个 std::overflow_error,其消息与之前通过 terminate() 打印出的 what() 消息完全相同。
To experiment with saturated arithmetic, change:
要尝试饱和算术,将:
using namespace bbi::term;
to:
改为:
using namespace bbi::sat;
Now, this program outputs the correct answer:
现在,这个程序输出正确的答案:
1
300 years is greater than 300ns.
300年比300纳秒大。
Another way to use this library to get the correct answer instead of overflow is to increase the range of nanoseconds to +/- far beyond the age of the universe. This can be done by having nanoseconds represented by a signed 128-bit type, instead of a 64-bit type. Change:
另一种使用这个库来获得正确答案、而不是溢出的方法,是将 nanoseconds 的取值范围扩大到远超宇宙年龄的+/- 范围。这可以通过让 nanoseconds 使用一个带符号的128位类型来表示,而不是64位类型来实现。将:
using nanoseconds = std::chrono::duration<i64, std::nano>;
to:
改为:
using nanoseconds = std::chrono::duration<i128, std::nano>;
Now, this program outputs the correct answer:
现在,这个程序输出正确的答案:
1
300 years is greater than 300ns.
300年比300纳秒大。
You can no longer have a zero-overhead release build with this setting, as there is no portable 128-bit type (until we get a portable _BitInt(128)), but you can remove the overflow checks by changing:
在这个设置下,你将再也无法拥有零开销的发布构建,因为没有可移植的128位类型(直到我们得到一个可移植的 _BitInt(128)),但你可以通过将以下改为来移除溢出检查:
using namespace bbi::term;
to:
改为:
using namespace bbi::wrap;
1 I am the main author of bbi. I am not seeking any financial benefit from this library. I consider helping people with C++ in general, and chrono in particular, a hobby of mine.
1我是 bbi 的主要作者。我并不为这个库寻求任何经济利益。我把帮助人们学习C++,特别是chrono,视为我的爱好。