Я использовал std::chrono
в течение многих лет и смотрел многие из Говарда Хиннанта
рассказывает о дизайне и использовании библиотеки. Мне это нравится, и я думаю, что в целом
понимать это. Однако недавно я вдруг понял, что не знаю, как
используйте его практически и безопасно , чтобы избежать неопределенного поведения.
Пожалуйста, потерпите меня, пока я иду через несколько случаев, чтобы подготовить почву для моего
вопрос.
Давайте начнем с того, что я считаю «самым простым» типом std::chrono::duration
,
nanoseconds
. Его минимальный размер rep
составляет 64 бита, что означает на практике
будет std::int64_t
, и, таким образом, вероятно, нет "остатка" необязательно
биты представления, которые не требуются в стандарте.
Эта функция, очевидно, не всегда безопасна:
nanoseconds f1(nanoseconds value)
{ return ++value; }
Если value
равно nanoseconds::max()
, то это переполняется, что мы можем подтвердить
с UBSan clang 7 (-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())
. Но этот случай
даже не должен бросать, так почему же он потерпел неудачу?
Ответ в том, что даже акт сравнения 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
(в это конкретная реализация стандартной библиотеки с ее
определенный 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+
Но есть некоторые довольно серьезные ограничения. Во-первых, мы должны были «знать», какконвертировать между 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>>
, я не вижу
способ действовать с уверенностью. Но даже если мы ограничимся "нормальным"
duration
типов, есть много страшных результатов.
В некотором смысле эта ситуация не «хуже», чем работа с обычным фиксированным размером
целые числа, как каждый делает каждый день, где вы часто «игнорируете» возможность
переполнения, потому что вы «знаете» область значений, с которыми работаете. Но,
на удивление, эти типы решений кажутся «хуже» с std::chrono
чем они делают с обычными целыми числами, потому что, как только вы пытаетесь быть в безопасности с
в отношении переполнения, вы в конечном итоге победили преимущества использования std::chrono
в
первое место.
Если я создаю свои duration
типы на основе беззнаковых rep
, я полагаю,
технически избегать хотя бы некоторого неопределенного поведения из целого числа
переполнение точки зрения, но я все еще могу легко получить результаты мусора из
«неосторожные» расчеты. «Проблемное пространство» кажется тем же.
Меня не интересует решение, основанное на типах с плавающей запятой. я использую
std::chrono
, чтобы сохранить точное разрешение, которое я выбираю в каждом конкретном случае. Если я
не заботился о точности или округлении ошибок, я мог легко использовать
double
чтобы считать секунды везде, а не смешивать юниты. Но если бы это было
жизнеспособное решение для каждой проблемы, у нас не было бы std::chrono
(или даже
struct timespec
, в этом отношении).
Итак, мой вопрос, как мне безопасно и практически использовать std::chrono
, чтобы сделать
что-то простое, как сложение двух значений разной продолжительности без
боязнь неопределенного поведения из-за целочисленного переполнения? Или сделать без потерь
конверсии безопасно? Я не нашел практического решения даже с известными
простые duration
типы, не говоря уже о богатой вселенной всевозможных duration
типы. Чего мне не хватает?