[N.B. I have almost completely rewritten this answer since I first posted it.]
The answer to your first question is, C has so many types in an attempt to balance the needs of supporting machines of all different word sizes, with reasonable portability. Things get more complicated due to the desire to also support specialized quantities like "sizes of data structures", "offsets in files", and "times in the real world" reasonably portably, and in spite of the fact that sometimes those specialized quantities end up being determined not by the language spec or the compiler but rather by the underlying operating system.
In general, there are two concerns when converting from a large integral type to a floating-point type:
the floating-point type might not be able to represent all the significant digits of the integral type accurately
the floating-point type might not even be able to handle the range of the integral type
In the case of time_t
, there is an additional concern:
- Type
time_t
might not be an integer number of seconds that you can meaningfully subtract
(These days, though, the "helpful" compiler warnings in response to these concerns do sometimes seem to border on the nannyish, and I share your concern. It can be hard to understand what obscure situation the compiler is actually worried about, and it can be hard to see how to rewrite the code without warnings, and it can be hard to be sure that any casts you end up having to insert don't end up making the code even less safe.)
If you're not worried about concern #3 (if you're willing to assume that time_t
is an integer number of seconds), you can minimize the chances for data loss by doing the subtraction first (and in the integral type), and then converting:
return (sometype)(end->tv_sec - start->tv_sec) +
(sometype)(end->tv_usec - start->tv_usec) / 1e6;
But of course the big question is, what should sometype be?
I believe your best bet is to cast to double
in each of those places. Both the guaranteed range and precision of type double
in C are quite large. So unless you're manipulating time differences greater than 1e50 years (and unless someone has implemented type subsec_t
to be a 266-bit type or something), your code should be safe even with the warning-suppressing casts, and you can insert a comment to that effect.
If you want to understand how these concerns can actually manifest in practice, it's easy to demonstrate them. Try this:
float f = (float)2000000123L - (float)2000000000L;
printf("%f\n", f);
If you have a 64-bit compiler, you can observe precision loss even with double precision:
double d = (double)9000000000000001234LL - (double)9000000000000000000LL;
printf("%f\n", d);
On my machine, these two fragments print 128
and 1024
, respectively.
I'm not sure exactly which of the three concerns your compiler was trying to warn you about. #1 is the likeliest possibility. And you can see how the precision loss disappears if you convert after subtracting, instead of before:
f = 2000000123L - 2000000000L;
d = 9000000000000001234LL - 9000000000000000000LL;
or
f = (float)(2000000123L - 2000000000L);
d = (double)(9000000000000001234LL - 9000000000000000000LL);
Back when all we had was 32-bit longs and 64-bit doubles, this wasn't much of a concern in practice (because IEEE 754 double
s have something like 52 bits of precision). Now that 64-bit types are becoming commonplace, these kinds of warnings are becoming much more widespread. If you're satisfied that type double
has enough precision for all your time-subtracting needs, you can use appropriate casts to double
to silence the warnings. (And, again, here by "appropriate" we mean "after the subtraction".) If you want to be even safer, you could move to type long double
instead. If your compiler supports it, and if it is indeed "longer" than regular double
, it can truly reduce the loss-of-precision problem. (Here's the previous example using long double
:
long double ld = (long double)9000000000000001234LL - (long double)9000000000000000000LL;
printf("%Lf\n", ld);
On my system this one prints 1234
.)
But with all of that said, in this case, if you want to really make your life easier -- and, incidentally, address concern #3 at the same time -- you could and arguably should use a standard function for computing the differences. The standard function for subtracting two time_t
values is difftime
. (It's difftime
's job to worry about all of these things, including the possibility that time_t
doesn't represent seconds directly.) So you could write
return difftime(end->tv_sec - start->tv_sec) +
(double)(end->tv_usec - start->tv_usec) / 1e6;
although of course there's still the problem of the subseconds.
The very best solution would be a prewritten library function to subtract two timeval
's, and you might want to spend some time looking for one of those.