Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why acts std::chrono::duration::operator*= not like built-in *=?

As described in std::chrono::duration::operator+= the signature is

duration& operator*=(const rep& rhs);

This makes me wonder. I would assume that a duration literal can be used like any other built-in, but it doesn't.

#include <chrono>
#include <iostream>

int main()
{
    using namespace std::chrono_literals;
    auto m = 10min;
    m *= 1.5f;
    std::cout << " 150% of 10min: " << m.count() << "min" << std::endl;

    int i = 10;
    i *= 1.5f;
    std::cout << " 150% of 10: " << i << std::endl;
}

Output is

150% of 10min: 10min
150% of 10: 15

Why was the interface choosen that way? To my mind, an interface like

template<typename T> 
duration& operator*=(const T& rhs);

would yield more intuitive results.

EDIT:
Thanks for your responses, I know that the implementation behaves that way and how I could handle it. My question is, why is it designed that way.

I would expect the conversion to int take place at the end of the operation. In the following example both operands get promoted to double before the multiplications happens. The intermediate result of 4.5 is converted to int afterwards, so that the result is 4.

int i = 3;
i *= 1.5;
assert(i == 4);

My expectation for std::duration would be that it behaves the same way.

like image 306
Michael Reinhardt Avatar asked Feb 22 '19 14:02

Michael Reinhardt


3 Answers

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;.

like image 165
NathanOliver Avatar answered Oct 07 '22 12:10

NathanOliver


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++.

like image 20
Howard Hinnant Avatar answered Oct 07 '22 12:10

Howard Hinnant


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 :-)

like image 35
Stack Danny Avatar answered Oct 07 '22 12:10

Stack Danny