gpt4 book ai didi

c++ - 我如何才能真正使用std::chrono类型而不冒溢出和未定义行为的风险?

转载 作者:行者123 更新时间:2023-12-01 12:08:59 27 4
gpt4 key购买 nike

我已经使用std::chrono多年了,并且看了很多Howard Hinnant的
讨论了库的设计和使用。我喜欢,而且我总体上认为
明白它。但是,最近,我突然意识到我不知道该怎么做
切实安全地使用它,以避免不确定的行为。

在我处理一些案例时,请多包涵我为我准备的舞台
问题。

让我们从我认为是“最简单”的std::chrono::duration类型开始,nanoseconds。其最小rep大小为64位,这意味着实际上
将是std::int64_t,因此可能没有“剩余”可选
标准不需要的代表性位。

此功能显然并不总是安全的:

nanoseconds f1(nanoseconds value)
{ return ++value; }

如果 valuenanoseconds::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值,我们想添加另一个
(未知) 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())。但是那种情况
甚至不应该扔,那为什么会失败呢?

答案是,即使比较 hoursnanoseconds的行为也涉及
转换。这是因为比较运算符是通过 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(关于此特定的标准库实现,及其
特定的 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+

但是,存在一些相当严重的局限性。首先,我们必须“知道”如何
hoursnanoseconds之间转换,这会破坏重点。
其次,这只能以非常好的方式处理这两种非常特殊的类型
它们的 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);
}

如果 duration1duration2确实可以是任何 duration类型(包括
std::chrono::duration<std::int16_t, std::ratio<3, 7>>这样的东西,我看不到
一种信心满满的方式。但是,即使我们将自己限制为“正常” duration类型,有很多可怕的结果。

在某些方面,这种情况比处理正常的固定大小更“糟糕”
整数,就像每个人每天都在做的一样,您经常“忽略”这种可能性
之所以会发生溢出,是因为您“知道”正在使用的值的域。但,
令我惊讶的是,对于 std::chrono,这些类型的解决方案似乎“更糟”
比起普通整数,因为一旦您尝试安全,
考虑到溢出,您最终将无法使用 std::chrono第一名。

如果我根据未签名的 duration创建自己的 rep类型,我想我
从技术上避免至少一些未定义的行为来自整数
溢出的观点,但我仍然可以轻松地从中获取垃圾结果
“粗心”的计算。 “问题空间”似乎是相同的。

我对基于浮点类型的解决方案不感兴趣。我正在使用 std::chrono可以保持我在每种情况下选择的精确分辨率。如果我
不在乎精确或四舍五入的错误,我可以轻松地使用 double可以在各处计算秒数,而不是混合使用单位。但是如果那是一个
每个问题的可行解决方案,我们甚至没有 std::chrono(甚至 struct timespec)。

所以我的问题是,如何安全,实用地使用 std::chrono进行操作
只需将两个持续时间不同的值相加而无需
担心由于整数溢出而导致不确定的行为?还是做无损
转换安全吗?即使已知,我也没有想出可行的解决方案
简单的 duration类型,更不用说所有可能的 duration的丰富世界
类型。我想念什么?

最佳答案

效果最好的答案是了解您的域,并且不要在所使用精度的最大范围附近进行编程。如果您使用nanoseconds,则范围为+/- 292年。不要走那么远。如果您需要的范围不仅仅是+/- 100年,请使用比纳秒更大的分辨率。

如果您可以遵循这些规则,那么您就不必担心溢出。

有时候你做不到。例如,如果您的代码必须处理不受信任的输入或常规输入(例如,通用库),那么您确实需要检查溢出。

一种技术是选择rep只是为了进行比较,它可以处理比任何人都需要的范围更大的范围。在这种情况下,int128_tdouble是我可以使用的两个工具。例如,这是一个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);
}

它要贵得多。但是,如果您正在编写(例如) 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是一件好事。

关于c++ - 我如何才能真正使用std::chrono类型而不冒溢出和未定义行为的风险?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54056353/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com