Bra*_*cer 4 c++ integer-overflow undefined-behavior c++-chrono
我已经使用std::chrono了多年,并观看了许多 Howard Hinnant 关于图书馆设计和使用的演讲。我喜欢它,我想我一般都理解它。然而,最近,我突然意识到我不知道如何实际和安全地使用它来避免未定义的行为。
在我通过几个案例为我的问题做准备时,请耐心等待。
让我们从我认为“最简单”的std::chrono::duration类型开始,
nanoseconds. 它的最小rep大小是 64 位,这意味着在实践中它会是std::int64_t,因此可能没有标准不需要的“剩余”可选表示位。
这个函数显然并不总是安全的:
nanoseconds f1(nanoseconds value)
{ return ++value; }
Run Code Online (Sandbox Code Playgroud)
如果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')
Run Code Online (Sandbox Code Playgroud)
但这没什么特别的。它与典型的整数情况没有什么不同:
std::int64_t f2(std::int64_t value)
{ return ++value; }
Run Code Online (Sandbox Code Playgroud)
当我们不能确定这value不是它的最大值时,我们首先检查,然后处理我们认为合适的错误。例如:
nanoseconds f3(nanoseconds value)
{
if(value == value.max())
{
throw std::overflow_error{"f3"};
}
return ++value;
}
Run Code Online (Sandbox Code Playgroud)
如果我们有一个现有的(未知)nanoseconds值想要添加另一个(未知)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; }
};
Run Code Online (Sandbox Code Playgroud)
再一次,我们会遇到麻烦:
runtime error: signed integer overflow: 9223372036854775807 +
9223372036854775807 cannot be represented in type 'long long'
Foo{}.f4(nanoseconds::max()) = -2 ns
Run Code Online (Sandbox Code Playgroud)
所以,同样,我们可以像处理整数一样做,但它已经变得更加棘手,因为这些是有符号整数:
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-
Run Code Online (Sandbox Code Playgroud)
我想我做对了。确定代码是否正确开始变得越来越困难。
到目前为止,事情似乎是可控的,但是这个案例呢?
nanoseconds f6(hours value)
{ return m_nanos + value; }
Run Code Online (Sandbox Code Playgroud)
我们遇到了与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;
}
Run Code Online (Sandbox Code Playgroud)
这看起来很合理,因为我们仍在检查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
Run Code Online (Sandbox Code Playgroud)
天啊。那肯定行不通。
在我的测试驱动程序中,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();
}
};
Run Code Online (Sandbox Code Playgroud)
由于我们没有检查我们的hours value是否足够小以适应
nanoseconds(在这个特定的标准库实现上,具有其特定的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)
Run Code Online (Sandbox Code Playgroud)
顺便说hours一句,如果使用 32 位,也会存在同样的问题rep:
runtime error: signed integer overflow: 2147483647 * 3600000000000 cannot be
represented in type 'long long'
Run Code Online (Sandbox Code Playgroud)
当然,如果我们把它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;
}
Run Code Online (Sandbox Code Playgroud)
如果我们根据标准库选择的限制对其进行测试,它似乎可以工作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+
Run Code Online (Sandbox Code Playgroud)
但是,有一些非常严重的限制。首先,我们必须“知道”如何在hours和之间进行转换nanoseconds,这样就没有意义了。其次,这只处理这两种非常特殊的类型,它们的period类型之间有很好的关系(只需要一个乘法)。
想象一下,我们只想实现标准命名duration类型的溢出安全转换,仅支持无损转换:
template <typename target_duration, typename source_duration>
target_duration lossless(source_duration duration)
{
// ... ?
}
Run Code Online (Sandbox Code Playgroud)
似乎我们需要计算比率之间的关系并基于此做出决策并检查乘法。. . 一旦我们这样做了,我们就必须理解并重新实现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;
}
Run Code Online (Sandbox Code Playgroud)
或者,更糟糕的是,即使我知道一些关于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);
}
Run Code Online (Sandbox Code Playgroud)
如果duration1并且duration2真的可以是任何duration类型(包括诸如 之类的东西std::chrono::duration<std::int16_t, std::ratio<3, 7>>,我看不出有信心继续前进的方法。但即使我们将自己限制为“正常”
duration类型,也会有很多可怕的结果。
在某些方面,这种情况并不比处理普通的固定大小整数“更糟糕”,就像每个人每天所做的那样,您经常“忽略”溢出的可能性,因为您“知道”正在使用的值域。但是,令我惊讶的是,这些类型的解决方案似乎std::chrono
比使用普通整数“更糟糕”,因为一旦您尝试在溢出方面保持安全,您最终就会失去使用的好处std::chrono。
如果我duration基于 unsigned制作我自己的类型rep,我想我在技术上至少从整数溢出的角度避免了一些未定义的行为,但我仍然可以很容易地从“粗心”计算中得到垃圾结果。“问题空间”似乎是一样的。
我对基于浮点类型的解决方案不感兴趣。我
std::chrono用来保持我在每种情况下选择的精确分辨率。如果我不关心精确度或四舍五入的错误,我可以轻松地在double任何地方使用
秒数而不是混合单位。但是,如果这是针对每个问题的可行解决方案,我们就不会std::chrono(甚至
struct timespec,就此而言)。
所以我的问题是,我如何安全且实用std::chrono地做一些简单的事情,比如将两个不同持续时间的值相加,而不必担心由于整数溢出而导致的未定义行为?还是安全地进行无损转换?即使使用已知的简单duration类型,我也没有想出一个实用的解决方案,更不用说所有可能duration
类型的丰富世界了。我错过了什么?
性能最高的答案是了解您的领域,并且不要在接近您使用的最大精度范围的任何地方进行编程。如果您使用nanoseconds的是 ,则范围为 +/- 292 年。不要靠近那么远。如果您需要的范围不仅仅是 +/- 100 年,请使用比纳秒更粗的分辨率。
如果您可以遵循这些规则,那么您就不必担心溢出。
有时你不能。例如,如果您的代码必须处理不受信任的输入或通用输入(例如通用库),那么您确实需要检查溢出。
一种技术是选择一种rep仅用于比较的可以处理比任何人都需要的范围更大的范围的仅用于比较。 int128_t并且double是我在这种情况下使用的两个工具。例如,这是在实际执行之前checked_convert检查溢出使用double的duration_cast:
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);
}
Run Code Online (Sandbox Code Playgroud)
它要贵得多。但是,如果您正在写作(例如)std::thread::sleep_for,那么花这笔钱是值得的。
如果由于某种原因您甚至无法使用浮点数进行检查,我已经尝试过lcm_type(不是一个好名字)。这与common_type_t<Duration1, Duration2>. 它不是找到duration两个输入durations 都可以无损失(没有除法)转换为的 ,而是找到duration两个输入durations 无需乘法即可转换为的 。例如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;
Run Code Online (Sandbox Code Playgroud)
您可以将两个输入持续时间都转换为lcm_type_t<Duration1, Duration2>,而不必担心溢出,然后进行比较。
这种技术的问题在于它不精确。两个略有不同的持续时间可能会转换为lcm_type_t并且由于截断损失,比较相等。出于这个原因,我更喜欢带有 的解决方案double,但lcm_type在您的工具箱中也有很好的解决方案。