我如何实际使用 std::chrono 类型而不冒溢出和未定义行为的风险?
How can I actually use std::chrono types without risking overflow and undefined behavior?
我用 std::chrono
很多年了,看过很多 Howard Hinnant 的作品
谈论图书馆的设计和使用。我喜欢它,我想我一般
明白它。然而,最近,我突然意识到,我不知道如何
实用地和安全地使用它来避免未定义的行为。
请耐心等待我通过几个案例来为我的
问题。
先说我认为是"simplest"std::chrono::duration
类型的,
nanoseconds
。它的最小 rep
大小是 64 位,这意味着实际上它
将是 std::int64_t
因此可能没有 "leftover" 可选
标准不需要的代表性位。
这个函数显然并不总是安全的:
nanoseconds f1(nanoseconds value)
{ return ++value; }
如果value
为nanoseconds::max()
,则溢出,我们可以确认
使用 clang 7 的 UBSan (-fsanitize=undefined
):
runtime error: signed integer overflow: 9223372036854775807 + 1 cannot be
represented in type 'std::__1::chrono::duration<long long,
std::__1::ratio<1, 1000000000> >::rep' (aka 'long long')
但这没什么特别的。它与典型的整数情况没有什么不同:
std::int64_t f2(std::int64_t value)
{ return ++value; }
当我们不能确定 value
不是它的最大值时,我们首先检查,
并以我们认为合适的方式处理错误。例如:
nanoseconds f3(nanoseconds value)
{
if(value == value.max())
{
throw std::overflow_error{"f3"};
}
return ++value;
}
如果我们有一个现有的(未知)nanoseconds
值,我们想添加另一个
(unknown) nanoseconds
值,天真的做法是:
struct Foo
{
// Pretend this can be set in other meaningful ways so we
// don't know what it is.
nanoseconds m_nanos = nanoseconds::max();
nanoseconds f4(nanoseconds value)
{ return m_nanos + value; }
};
再一次,我们会遇到麻烦:
runtime error: signed integer overflow: 9223372036854775807 +
9223372036854775807 cannot be represented in type 'long long'
Foo{}.f4(nanoseconds::max()) = -2 ns
所以,再一次,我们可以像处理整数一样做,但它已经
更棘手,因为这些是有符号整数:
struct Foo
{
explicit Foo(nanoseconds nanos = nanoseconds::max())
: m_nanos{nanos}
{}
// Again, pretend this can be set in other ways, so we don't
// know what it is.
nanoseconds m_nanos;
nanoseconds f5(nanoseconds value)
{
if(m_nanos > m_nanos.zero() && value > m_nanos.max() - m_nanos)
{
throw std::overflow_error{"f5+"};
}
else if(m_nanos < m_nanos.zero() && value < m_nanos.min() - m_nanos)
{
throw std::overflow_error{"f5-"};
}
return m_nanos + value;
}
};
Foo{}.f5(0ns) = 9223372036854775807 ns
Foo{}.f5(nanoseconds::min()) = -1 ns
Foo{}.f5(1ns) threw std::overflow_error: f5+
Foo{}.f5(nanoseconds::max()) threw std::overflow_error: f5+
Foo{nanoseconds::min()}.f5(0ns) = -9223372036854775808 ns
Foo{nanoseconds::min()}.f5(nanoseconds::max()) = -1 ns
Foo{nanoseconds::min()}.f5(-1ns) threw std::overflow_error: f5-
Foo{nanoseconds::min()}.f5(nanoseconds::min()) threw std::overflow_error: f5-
我认为我是对的。开始变得越来越难确定
代码正确。
到目前为止,事情似乎是可以解决的,但是这个案例呢?
nanoseconds f6(hours value)
{ return m_nanos + value; }
我们遇到了与 f4()
相同的问题。我们可以用同样的方法解决吗
f5()
做了吗?让我们使用与 f5()
相同的正文,但只需更改参数
输入,看看会发生什么:
nanoseconds f7(hours value)
{
if(m_nanos > m_nanos.zero() && value > m_nanos.max() - m_nanos)
{
throw std::overflow_error{"f7+"};
}
else if(m_nanos < m_nanos.zero() && value < m_nanos.min() - m_nanos)
{
throw std::overflow_error{"f7-"};
}
return m_nanos + value;
}
这看起来很合理,因为我们仍在检查两者之间是否有空间
nanoseconds::max()
和 m_nanos
添加到 value
。那么当我们
运行这个?
Foo{}.f7(0h) = 9223372036854775807 ns
/usr/lib/llvm-7/bin/../include/c++/v1/chrono:880:59: runtime error: signed
integer overflow: -9223372036854775808 * 3600000000000 cannot be represented
in type 'long long'
Foo{}.f7(hours::min()) = 9223372036854775807 ns
Foo{}.f7(1h) threw std::overflow_error: f7+
Foo{}.f7(hours::max()) DIDN'T THROW!!!!!!!!!!!!!!
Foo{nanoseconds::min()}.f7(0h) = -9223372036854775808 ns
terminating with uncaught exception of type std::overflow_error: f7-
Aborted
天哪。那绝对不行。
在我的测试驱动程序中,UBSan 错误打印在调用上方
报告,所以第一次失败是Foo{}.f7(hours::min())
。但是那个案子
甚至不应该抛出,为什么会失败?
答案是即使 比较 hours
和 nanoseconds
的行为也涉及
转换。这是因为比较运算符是通过
使用 std::common_type
,其中 std::chrono
为 duration
类型定义
period
值的最大公约数项。在我们的例子中,
那是 nanoseconds
,所以首先,hours
被转换为 nanoseconds
。一个
libc++
的片段显示了其中的一部分:
template <class _LhsDuration, class _RhsDuration>
struct __duration_lt
{
_LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR
bool operator()(const _LhsDuration& __lhs, const _RhsDuration& __rhs) const
{
typedef typename common_type<_LhsDuration, _RhsDuration>::type _Ct;
return _Ct(__lhs).count() < _Ct(__rhs).count();
}
};
因为我们没有检查我们的 hours
value
是否足够小以适应
nanoseconds
(关于 this 特定的标准库实现,及其
特定 rep
类型选择),以下本质上是等价的:
if(m_nanos > m_nanos.zero() && value > m_nanos.max() - m_nanos)
if(m_nanos > m_nanos.zero() && nanoseconds{value} > m_nanos.max() - m_nanos)
顺便说一句,如果hours
使用32位rep
也会出现同样的问题:
runtime error: signed integer overflow: 2147483647 * 3600000000000 cannot be
represented in type 'long long'
当然,如果我们使 value
足够小,包括通过限制 rep
大小,我们最终可以让它合身。 . .因为显然 一些 hours
值
可以表示为 nanoseconds
否则转换将毫无意义。
我们还没有放弃。无论如何,转换是另一个重要的案例,所以我们
应该知道如何安全地处理它们。当然这不会太难。
第一个障碍是我们需要知道我们是否可以从 hours
到
nanoseconds
而不会溢出 nanoseconds::rep
类型。再一次,像我们一样
将使用整数并进行乘法溢出检查。目前,
让我们忽略负值。我们可以这样做:
nanoseconds f8(hours value)
{
assert(value >= value.zero());
if(value.count()
> std::numeric_limits<nanoseconds::rep>::max() / 3600000000000)
{
throw std::overflow_error{"f8+"};
}
return value;
}
如果我们根据标准库的限制对其进行测试,它似乎可以工作
选择nanoseconds::rep
:
f8(0h) = 0 ns
f8(1h) = 3600000000000 ns
f8(2562047h) = 9223369200000000000 ns
f8(2562048h) threw std::overflow_error: f8+
f8(hours::max()) threw std::overflow_error: f8+
但是,有一些非常严重的限制。首先,我们必须 "know" 如何
在 hours
和 nanoseconds
之间转换,这有点不合时宜。
其次,这只处理这两种非常特殊的类型,并且非常好
它们的 period
类型之间的关系(其中只有一个乘法是
需要)。
想象一下,我们只想实现标准的溢出安全转换
已命名 duration
类型,仅支持无损转换:
template <typename target_duration, typename source_duration>
target_duration lossless(source_duration duration)
{
// ... ?
}
看来我们需要计算比率之间的关系并做出决定
并根据那个检查乘法。 . .一旦我们做到了,
我们必须理解并重新实现 duration
中的所有逻辑
我们最初打算在其中使用的运算符(但现在具有溢出安全性)
第一名!我们真的不需要实现类型只是为了使用
输入,我们可以吗?
此外,完成后,我们还有一些函数 lossless()
,它执行
如果我们显式调用它而不是允许自然隐式转换
转换,或者如果我们显式调用它会增加值的其他一些函数
而不是使用 operator+()
,所以我们失去了巨大的表现力
duration
.
的部分值
将有损转换与 duration_cast
添加到混合有损转换中,这似乎没有希望。
我什至不确定我将如何处理像这样简单的事情:
template <typename duration1, typename duration2>
bool isSafe(duration1 limit, duration2 reading)
{
assert(limit >= limit.zero());
return reading < limit / 2;
}
或者,更糟的是,即使我知道一些关于 grace
的事情:
template <typename duration1, typename duration2>
bool isSafe2(duration1 limit, duration2 reading, milliseconds grace)
{
assert(limit >= limit.zero());
assert(grace >= grace.zero());
const auto test = limit / 2;
return grace < test && reading < (test - grace);
}
如果duration1
和duration2
真的可以是任何duration
类型(包括
std::chrono::duration<std::int16_t, std::ratio<3, 7>>
之类的东西,我看不到
一种充满信心地进行的方法。但即使我们将自己限制在 "normal"
duration
种,有很多可怕的结果。
在某些方面,这种情况并不 "worse" 处理正常的固定大小
整数,就像每个人每天都做的那样,你经常 "ignore" 的可能性
溢出,因为您 "know" 您正在使用的值域。但,
令我惊讶的是,这些类型的解决方案似乎 "worse" 和 std::chrono
比他们对普通整数所做的要多,因为一旦您尝试确保安全
关于溢出,你最终会失去使用 std::chrono
的好处
第一名。
如果我根据无符号 rep
创建自己的 duration
类型,我想我
从技术上至少避免整数的一些未定义行为
溢出的观点,但我仍然可以很容易地从
"careless" 计算。 "problem space"好像是一样的
我对基于浮点类型的解决方案不感兴趣。我在用着
std::chrono
以保持我在每种情况下选择的精确分辨率。如果我
不关心精确或舍入误差,我可以很容易地使用
double
到处都计算秒数,而不是混合单位。但如果那是一个
每个问题的可行解决方案,我们不会有 std::chrono
(甚至
struct timespec
,就此而言)。
所以我的问题是,如何安全实用地使用 std::chrono
来做
就像将两个不同持续时间的值加在一起一样简单
害怕因整数溢出而导致未定义的行为?或者做无损
安全转换?即使已知,我也没有想出一个实用的解决方案
简单的 duration
类型,更不用说所有可能的丰富宇宙 duration
类型。我错过了什么?
最好的答案是了解您的域,并且不要在您使用的最大精度范围附近的任何地方编程。如果您使用 nanoseconds
,范围是 +/- 292 年。不要靠近那么远。如果您需要的范围不仅仅是 +/- 100 年,请使用比纳秒更粗略的分辨率。
如果你能遵守这些规则,那么你就不用担心溢出了。
有时你不能。例如,如果您的代码必须处理不受信任的输入或一般输入(例如通用库),那么您确实需要检查溢出。
一种技术是选择一个 rep
仅用于比较,它可以处理比任何人需要的范围更大的范围,仅用于比较。 int128_t
和 double
是我在这种情况下使用的两个工具。例如,这里有一个 checked_convert
在实际执行 duration_cast
:
之前使用 double
检查溢出
template <class Duration, class Rep, class Period>
Duration
checked_convert(std::chrono::duration<Rep, Period> d)
{
using namespace std::chrono;
using S = duration<double, typename Duration::period>;
constexpr S m = Duration::min();
constexpr S M = Duration::max();
S s = d;
if (s < m || s > M)
throw std::overflow_error("checked_convert");
return duration_cast<Duration>(d);
}
它明显更贵。但是如果你写的是(比如)std::thread::sleep_for
,那是值得的。
如果出于某种原因您甚至无法使用浮点数进行检查,我已经尝试过 lcm_type
(不是一个好名字)。这与 common_type_t<Duration1, Duration2>
相反。它没有找到两个输入 duration
都可以无损(无除法)转换为的 duration
,而是找到两个输入 duration
可以无损转换为的 duration
一个乘法。例如 lcm_type_t<milliseconds, nanoseconds>
的类型为 milliseconds
。 这样的转换不能溢出
template <class Duration0, class ...Durations>
struct lcm_type;
template <class Duration>
struct lcm_type<Duration>
{
using type = Duration;
};
template <class Duration1, class Duration2>
struct lcm_type<Duration1, Duration2>
{
template <class D>
using invert = std::chrono::duration
<
typename D::rep,
std::ratio_divide<std::ratio<1>, typename D::period>
>;
using type = invert<typename std::common_type<invert<Duration1>,
invert<Duration2>>::type>;
};
template <class Duration0, class Duration1, class Duration2, class ...Durations>
struct lcm_type<Duration0, Duration1, Duration2, Durations...>
{
using type = typename lcm_type<
typename lcm_type<Duration0, Duration1>::type,
Duration2, Durations...>::type;
};
template <class ...T>
using lcm_type_t = typename lcm_type<T...>::type;
你可以将两个输入时长转换为lcm_type_t<Duration1, Duration2>
,不用担心溢出,然后进行比较。
这种技术的问题是它不精确。两个略有不同的持续时间可能会转换为 lcm_type_t
并且由于截断损失,比较相等。出于这个原因,我更喜欢 double
的解决方案,但最好在你的工具箱中也有 lcm_type
。
我用 std::chrono
很多年了,看过很多 Howard Hinnant 的作品
谈论图书馆的设计和使用。我喜欢它,我想我一般
明白它。然而,最近,我突然意识到,我不知道如何
实用地和安全地使用它来避免未定义的行为。
请耐心等待我通过几个案例来为我的 问题。
先说我认为是"simplest"std::chrono::duration
类型的,
nanoseconds
。它的最小 rep
大小是 64 位,这意味着实际上它
将是 std::int64_t
因此可能没有 "leftover" 可选
标准不需要的代表性位。
这个函数显然并不总是安全的:
nanoseconds f1(nanoseconds value)
{ return ++value; }
如果value
为nanoseconds::max()
,则溢出,我们可以确认
使用 clang 7 的 UBSan (-fsanitize=undefined
):
runtime error: signed integer overflow: 9223372036854775807 + 1 cannot be
represented in type 'std::__1::chrono::duration<long long,
std::__1::ratio<1, 1000000000> >::rep' (aka 'long long')
但这没什么特别的。它与典型的整数情况没有什么不同:
std::int64_t f2(std::int64_t value)
{ return ++value; }
当我们不能确定 value
不是它的最大值时,我们首先检查,
并以我们认为合适的方式处理错误。例如:
nanoseconds f3(nanoseconds value)
{
if(value == value.max())
{
throw std::overflow_error{"f3"};
}
return ++value;
}
如果我们有一个现有的(未知)nanoseconds
值,我们想添加另一个
(unknown) nanoseconds
值,天真的做法是:
struct Foo
{
// Pretend this can be set in other meaningful ways so we
// don't know what it is.
nanoseconds m_nanos = nanoseconds::max();
nanoseconds f4(nanoseconds value)
{ return m_nanos + value; }
};
再一次,我们会遇到麻烦:
runtime error: signed integer overflow: 9223372036854775807 +
9223372036854775807 cannot be represented in type 'long long'
Foo{}.f4(nanoseconds::max()) = -2 ns
所以,再一次,我们可以像处理整数一样做,但它已经 更棘手,因为这些是有符号整数:
struct Foo
{
explicit Foo(nanoseconds nanos = nanoseconds::max())
: m_nanos{nanos}
{}
// Again, pretend this can be set in other ways, so we don't
// know what it is.
nanoseconds m_nanos;
nanoseconds f5(nanoseconds value)
{
if(m_nanos > m_nanos.zero() && value > m_nanos.max() - m_nanos)
{
throw std::overflow_error{"f5+"};
}
else if(m_nanos < m_nanos.zero() && value < m_nanos.min() - m_nanos)
{
throw std::overflow_error{"f5-"};
}
return m_nanos + value;
}
};
Foo{}.f5(0ns) = 9223372036854775807 ns
Foo{}.f5(nanoseconds::min()) = -1 ns
Foo{}.f5(1ns) threw std::overflow_error: f5+
Foo{}.f5(nanoseconds::max()) threw std::overflow_error: f5+
Foo{nanoseconds::min()}.f5(0ns) = -9223372036854775808 ns
Foo{nanoseconds::min()}.f5(nanoseconds::max()) = -1 ns
Foo{nanoseconds::min()}.f5(-1ns) threw std::overflow_error: f5-
Foo{nanoseconds::min()}.f5(nanoseconds::min()) threw std::overflow_error: f5-
我认为我是对的。开始变得越来越难确定 代码正确。
到目前为止,事情似乎是可以解决的,但是这个案例呢?
nanoseconds f6(hours value)
{ return m_nanos + value; }
我们遇到了与 f4()
相同的问题。我们可以用同样的方法解决吗
f5()
做了吗?让我们使用与 f5()
相同的正文,但只需更改参数
输入,看看会发生什么:
nanoseconds f7(hours value)
{
if(m_nanos > m_nanos.zero() && value > m_nanos.max() - m_nanos)
{
throw std::overflow_error{"f7+"};
}
else if(m_nanos < m_nanos.zero() && value < m_nanos.min() - m_nanos)
{
throw std::overflow_error{"f7-"};
}
return m_nanos + value;
}
这看起来很合理,因为我们仍在检查两者之间是否有空间
nanoseconds::max()
和 m_nanos
添加到 value
。那么当我们
运行这个?
Foo{}.f7(0h) = 9223372036854775807 ns
/usr/lib/llvm-7/bin/../include/c++/v1/chrono:880:59: runtime error: signed
integer overflow: -9223372036854775808 * 3600000000000 cannot be represented
in type 'long long'
Foo{}.f7(hours::min()) = 9223372036854775807 ns
Foo{}.f7(1h) threw std::overflow_error: f7+
Foo{}.f7(hours::max()) DIDN'T THROW!!!!!!!!!!!!!!
Foo{nanoseconds::min()}.f7(0h) = -9223372036854775808 ns
terminating with uncaught exception of type std::overflow_error: f7-
Aborted
天哪。那绝对不行。
在我的测试驱动程序中,UBSan 错误打印在调用上方
报告,所以第一次失败是Foo{}.f7(hours::min())
。但是那个案子
甚至不应该抛出,为什么会失败?
答案是即使 比较 hours
和 nanoseconds
的行为也涉及
转换。这是因为比较运算符是通过
使用 std::common_type
,其中 std::chrono
为 duration
类型定义
period
值的最大公约数项。在我们的例子中,
那是 nanoseconds
,所以首先,hours
被转换为 nanoseconds
。一个
libc++
的片段显示了其中的一部分:
template <class _LhsDuration, class _RhsDuration>
struct __duration_lt
{
_LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR
bool operator()(const _LhsDuration& __lhs, const _RhsDuration& __rhs) const
{
typedef typename common_type<_LhsDuration, _RhsDuration>::type _Ct;
return _Ct(__lhs).count() < _Ct(__rhs).count();
}
};
因为我们没有检查我们的 hours
value
是否足够小以适应
nanoseconds
(关于 this 特定的标准库实现,及其
特定 rep
类型选择),以下本质上是等价的:
if(m_nanos > m_nanos.zero() && value > m_nanos.max() - m_nanos)
if(m_nanos > m_nanos.zero() && nanoseconds{value} > m_nanos.max() - m_nanos)
顺便说一句,如果hours
使用32位rep
也会出现同样的问题:
runtime error: signed integer overflow: 2147483647 * 3600000000000 cannot be
represented in type 'long long'
当然,如果我们使 value
足够小,包括通过限制 rep
大小,我们最终可以让它合身。 . .因为显然 一些 hours
值
可以表示为 nanoseconds
否则转换将毫无意义。
我们还没有放弃。无论如何,转换是另一个重要的案例,所以我们 应该知道如何安全地处理它们。当然这不会太难。
第一个障碍是我们需要知道我们是否可以从 hours
到
nanoseconds
而不会溢出 nanoseconds::rep
类型。再一次,像我们一样
将使用整数并进行乘法溢出检查。目前,
让我们忽略负值。我们可以这样做:
nanoseconds f8(hours value)
{
assert(value >= value.zero());
if(value.count()
> std::numeric_limits<nanoseconds::rep>::max() / 3600000000000)
{
throw std::overflow_error{"f8+"};
}
return value;
}
如果我们根据标准库的限制对其进行测试,它似乎可以工作
选择nanoseconds::rep
:
f8(0h) = 0 ns
f8(1h) = 3600000000000 ns
f8(2562047h) = 9223369200000000000 ns
f8(2562048h) threw std::overflow_error: f8+
f8(hours::max()) threw std::overflow_error: f8+
但是,有一些非常严重的限制。首先,我们必须 "know" 如何
在 hours
和 nanoseconds
之间转换,这有点不合时宜。
其次,这只处理这两种非常特殊的类型,并且非常好
它们的 period
类型之间的关系(其中只有一个乘法是
需要)。
想象一下,我们只想实现标准的溢出安全转换
已命名 duration
类型,仅支持无损转换:
template <typename target_duration, typename source_duration>
target_duration lossless(source_duration duration)
{
// ... ?
}
看来我们需要计算比率之间的关系并做出决定
并根据那个检查乘法。 . .一旦我们做到了,
我们必须理解并重新实现 duration
中的所有逻辑
我们最初打算在其中使用的运算符(但现在具有溢出安全性)
第一名!我们真的不需要实现类型只是为了使用
输入,我们可以吗?
此外,完成后,我们还有一些函数 lossless()
,它执行
如果我们显式调用它而不是允许自然隐式转换
转换,或者如果我们显式调用它会增加值的其他一些函数
而不是使用 operator+()
,所以我们失去了巨大的表现力
duration
.
将有损转换与 duration_cast
添加到混合有损转换中,这似乎没有希望。
我什至不确定我将如何处理像这样简单的事情:
template <typename duration1, typename duration2>
bool isSafe(duration1 limit, duration2 reading)
{
assert(limit >= limit.zero());
return reading < limit / 2;
}
或者,更糟的是,即使我知道一些关于 grace
的事情:
template <typename duration1, typename duration2>
bool isSafe2(duration1 limit, duration2 reading, milliseconds grace)
{
assert(limit >= limit.zero());
assert(grace >= grace.zero());
const auto test = limit / 2;
return grace < test && reading < (test - grace);
}
如果duration1
和duration2
真的可以是任何duration
类型(包括
std::chrono::duration<std::int16_t, std::ratio<3, 7>>
之类的东西,我看不到
一种充满信心地进行的方法。但即使我们将自己限制在 "normal"
duration
种,有很多可怕的结果。
在某些方面,这种情况并不 "worse" 处理正常的固定大小
整数,就像每个人每天都做的那样,你经常 "ignore" 的可能性
溢出,因为您 "know" 您正在使用的值域。但,
令我惊讶的是,这些类型的解决方案似乎 "worse" 和 std::chrono
比他们对普通整数所做的要多,因为一旦您尝试确保安全
关于溢出,你最终会失去使用 std::chrono
的好处
第一名。
如果我根据无符号 rep
创建自己的 duration
类型,我想我
从技术上至少避免整数的一些未定义行为
溢出的观点,但我仍然可以很容易地从
"careless" 计算。 "problem space"好像是一样的
我对基于浮点类型的解决方案不感兴趣。我在用着
std::chrono
以保持我在每种情况下选择的精确分辨率。如果我
不关心精确或舍入误差,我可以很容易地使用
double
到处都计算秒数,而不是混合单位。但如果那是一个
每个问题的可行解决方案,我们不会有 std::chrono
(甚至
struct timespec
,就此而言)。
所以我的问题是,如何安全实用地使用 std::chrono
来做
就像将两个不同持续时间的值加在一起一样简单
害怕因整数溢出而导致未定义的行为?或者做无损
安全转换?即使已知,我也没有想出一个实用的解决方案
简单的 duration
类型,更不用说所有可能的丰富宇宙 duration
类型。我错过了什么?
最好的答案是了解您的域,并且不要在您使用的最大精度范围附近的任何地方编程。如果您使用 nanoseconds
,范围是 +/- 292 年。不要靠近那么远。如果您需要的范围不仅仅是 +/- 100 年,请使用比纳秒更粗略的分辨率。
如果你能遵守这些规则,那么你就不用担心溢出了。
有时你不能。例如,如果您的代码必须处理不受信任的输入或一般输入(例如通用库),那么您确实需要检查溢出。
一种技术是选择一个 rep
仅用于比较,它可以处理比任何人需要的范围更大的范围,仅用于比较。 int128_t
和 double
是我在这种情况下使用的两个工具。例如,这里有一个 checked_convert
在实际执行 duration_cast
:
double
检查溢出
template <class Duration, class Rep, class Period>
Duration
checked_convert(std::chrono::duration<Rep, Period> d)
{
using namespace std::chrono;
using S = duration<double, typename Duration::period>;
constexpr S m = Duration::min();
constexpr S M = Duration::max();
S s = d;
if (s < m || s > M)
throw std::overflow_error("checked_convert");
return duration_cast<Duration>(d);
}
它明显更贵。但是如果你写的是(比如)std::thread::sleep_for
,那是值得的。
如果出于某种原因您甚至无法使用浮点数进行检查,我已经尝试过 lcm_type
(不是一个好名字)。这与 common_type_t<Duration1, Duration2>
相反。它没有找到两个输入 duration
都可以无损(无除法)转换为的 duration
,而是找到两个输入 duration
可以无损转换为的 duration
一个乘法。例如 lcm_type_t<milliseconds, nanoseconds>
的类型为 milliseconds
。 这样的转换不能溢出
template <class Duration0, class ...Durations>
struct lcm_type;
template <class Duration>
struct lcm_type<Duration>
{
using type = Duration;
};
template <class Duration1, class Duration2>
struct lcm_type<Duration1, Duration2>
{
template <class D>
using invert = std::chrono::duration
<
typename D::rep,
std::ratio_divide<std::ratio<1>, typename D::period>
>;
using type = invert<typename std::common_type<invert<Duration1>,
invert<Duration2>>::type>;
};
template <class Duration0, class Duration1, class Duration2, class ...Durations>
struct lcm_type<Duration0, Duration1, Duration2, Durations...>
{
using type = typename lcm_type<
typename lcm_type<Duration0, Duration1>::type,
Duration2, Durations...>::type;
};
template <class ...T>
using lcm_type_t = typename lcm_type<T...>::type;
你可以将两个输入时长转换为lcm_type_t<Duration1, Duration2>
,不用担心溢出,然后进行比较。
这种技术的问题是它不精确。两个略有不同的持续时间可能会转换为 lcm_type_t
并且由于截断损失,比较相等。出于这个原因,我更喜欢 double
的解决方案,但最好在你的工具箱中也有 lcm_type
。