Howard E. Hinnant
2016-07-30

chrono_io

Contents

Introduction

This is a small library which does nothing but add non-configurable, non-localizable streaming to std::chrono::duration. The streaming operator lives in namespace date for convenience purposes, and does not live in namespace std::chrono as that would violate the standard.

The entire API of this library is:

namespace date
{

template <class CharT, class Traits, class Rep, class Period>
std::basic_ostream<CharT, Traits>&
operator<<(std::basic_ostream<CharT, Traits>& os,
           const std::chrono::duration<Rep, Period>& d);

}  // namespace date

Just one signature for an entire library? Are you serious?!

Very serious.

Parsing is not included because there are a million ways to parse durations, and none of them are universally agreed upon. And they are all easily implementable without private access to std::chrono::duration just by parsing arithmetic types and strings.

No formatting options?!

No.

The formatting provided here is based on an international SI standard. There is no need for localization. If you need other formatting options for streaming your durations that is easy enough for you to supply. What is needed here is zero-effort streaming of chrono::durations without hassle. Streaming chrono::durations is the biggest use of .count() (a dangerous type cast), and that use should be handled by a library such as this.

Examples

#include "chrono_io.h"
#include <iostream>

void f();

int
main()
{
    using namespace date;
    using namespace std::chrono;
    auto t0 = steady_clock::now();
    f();
    auto t1 = steady_clock::now();
    std::cout << t1 - t0 << '\n';
}

With this library, you don't have to specify the duration type (and cast to it) in timing situations like this. This will create whatever duration is native to steady_clock and print out the value and units for that duration, for example:

682580ns

Change steady_clock to system_clock (and if that clock has different units on your platform), and the output automatically changes units for you:

auto t0 = system_clock::now();
f();
auto t1 = system_clock::now();
std::cout << t1 - t0 << '\n';
682µs

And yes, it really does print out the Greek letter µ and not u. When streaming to char-based streams it uses UTF-8 encoding to represent the Unicode character 'MICRO SIGN' (U+00B5). Otherwise it uses UTF-16 or UTF-32 based on the size of character the stream is using.

In general, if the duration tick period is the same type as one of the non-optional std::ratio SI convenience typedefs (atto thru exa), then the unit will use the internationally accepted symbol for the metric prefix followed by 's'.

In addition, if the duration tick period is ratio<60>, then the printed unit is min. And if the duration tick period is ratio<3600>, then the unit is h.

std::cout << 1s << '\n';
std::cout << 2min << '\n';
std::cout << 3h << '\n';

Outputs:

1s
2min
3h

Sometimes odd durations pop up which have no widely recognized names. For example consider this code:

using frames = duration<int, ratio<1, 60>>;
std::cout << 45ms + frames{5} << '\n';

This sum can't be exactly represented as either milliseconds or frames (1/60 of a second). The compiler figures out the coarsest unit which can exactly represent the sum of any millisecond and any frame and auto-generates that unit to hold the sum. For this example that duration will have a tick period of 1/3000 of a second. This library puts that ratio inside of square brackets, and then appends 's':

385[1/3000]s

If the durations tick period is a whole number of seconds (period::den == 1), then the /1 is dropped inside of the square brackets. The following example uses months from date.h.

std::cout << months{6} << '\n';
6[2629746]s

Trivia: 2,629,746s is the exact length of the average Gregorian month, which is also exactly 30.436875 24h days.

Implementation Details

Question: Why are there separate implementations for C++11 and C++14?

Answer: The units of a duration are always known at compile time. Thus the string representation of the units is always known at compile time. This is true even when the unit is of the form [num/den]s. But only C++14 has the gravitas to do the complete string conversion at compile time. And now that I've claimed that, I'm sure someone will come up with a C++11 that VS-2015 won't compile anyway. So I need a run-time solution (based on std::basic_string<CharT>, and a compile-time solution that currently only gcc and clang with -std=c++14 can handle.

For example if you compile the following code with -std=c++14 and clang:

using frames = std::chrono::duration<int, std::ratio<1, 60>>;

void
test(std::ostream& os, std::chrono::milliseconds x, frames y)
{
    using namespace date;
    os << x + y;
}

And then inspect the generated assembly, it contains this:

movabsq	$6714920027984703835, %rax ## imm = 0x5D303030332F315B

Which is assembly-speak for "[1/3000]" all boiled down to one x86_64 instruction. The 's' wouldn't quite fit within this one instruction and follows in a later instruction. C++14 can do some amazing things at compile time! For C++11 I just gave up and called std::to_string.

Reference

template <class CharT, class Traits, class Rep, class Period>
std::basic_ostream<CharT, Traits>&
operator<<(std::basic_ostream<CharT, Traits>& os,
           const std::chrono::duration<Rep, Period>& d);

Effects: Equivalent to:

os << d.count() << detail::get_units<CharT>(duration<Rep, typename Period::type>{});

Where detail::get_units<CharT>() returns a null-terminated string of CharT which depends only on Period::type as follows (let period be the type Period::type):

  • If period is type std::atto, as, else
  • if period is type std::femto, fs, else
  • if period is type std::pico, ps, else
  • if period is type std::nano, ns, else
  • if period is type std::micro, µs (U+00B5), else
  • if period is type std::milli, ms, else
  • if period is type std::centi, cs, else
  • if period is type std::deci, ds, else
  • if period is type std::ratio<1>, s, else
  • if period is type std::deca, das, else
  • if period is type std::hecto, hs, else
  • if period is type std::kilo, ks, else
  • if period is type std::mega, Ms, else
  • if period is type std::giga, Gs, else
  • if period is type std::tera, Ts, else
  • if period is type std::peta, Ps, else
  • if period is type std::exa, Es, else
  • if period is type std::ratio<60>, min, else
  • if period is type std::ratio<3600>, h, else
  • if period::den == 1, [num]s, else
  • [num/den]s.

In the list above the use of num and den refer to the static data members of period which are converted to arrays of CharT using a decimal conversion with no leading zeroes.

Returns: os.