Why acts std::chrono::duration::operator*= not like built-in *=?
The issue here is
auto m = 10min;
gives you a std::chrono::duration
where rep
is a signed integer type. When you do
m *= 1.5f;
the 1.5f
is converted to the type rep
and that means it is truncated to 1
, which gives you the same value after multiplication.
To fix this you need to use
auto m = 10.0min;
to get a std::chrono::duration
that uses a floating point type for rep
and wont truncate 1.5f
when you do m *= 1.5f;
.
My question is, why is it designed that way.
It was designed this way (ironically) because the integral-based computations are designed to give exact results, or not compile. However in this case the <chrono>
library exerts no control over what conversions get applied to arguments prior to binding to the arguments.
As a concrete example, consider the case where m
is initialized to 11min
, and presume that we had a templated operator*=
as you suggest. The exact answer is now 16.5min
, but the integral-based type chrono::minutes
is not capable of representing this value.
A superior design would be to have this line:
m *= 1.5f; // compile-time error
not compile. That would make the library more self-consistent: Integral-based arithmetic is either exact (or requires duration_cast
) or does not compile. This would be possible to implement, and the answer as to why this was not done is simply that I didn't think of it.
If you (or anyone else) feels strongly enough about this to try to standardize a compile-time error for the above statement, I would be willing to speak in favor of such a proposal in committee.
This effort would involve:
- An implementation with unit tests.
- Fielding it to get a feel for how much code it would break, and ensuring that it does not break code not intended.
- Write a paper and submit it to the C++ committee, targeting C++23 (it is too late to target C++20).
The easiest way to do this would be to start with an open-source implementation such as gcc's libstdc++ or llvm's libc++.
Looking at the implementation of operator*=
:
_CONSTEXPR17 duration& operator*=(const _Rep& _Right)
{ // multiply rep by _Right
_MyRep *= _Right;
return (*this);
}
the operator takes a const _Rep&
. It comes from std::duration
which looks like:
template<class _Rep, //<-
class _Period>
class duration
{ // represents a time Duration
//...
So now if we look at the definition of std::chrono::minutes
:
using minutes = duration<int, ratio<60>>;
It is clear that _Rep
is an int
.
So when you call operator*=(const _Rep& _Right)
1.5f
is beeing cast to an int
- which equals 1
and therefore won't affect any mulitiplications with itself.
So what can you do?
you can split it up into m = m * 1.5f
and use std::chrono::duration_cast
to cast from std::chrono::duration<float, std::ratio>
to std::chrono::duration<int, std::ratio>
m = std::chrono::duration_cast<std::chrono::minutes>(m * 1.5f);
150% of 10min: 15min
if you don't like always casting it, use a float
for it as the first template argument:
std::chrono::duration<float, std::ratio<60>> m = 10min;
m *= 1.5f; //> 15min
or even quicker - auto m = 10.0min; m *= 1.5f;
as @NathanOliver answered :-)