0

I'm currently writing a templated c++ physics library. In my functions, I often have to explicitly compare or set certain numerical values. My goal is to write the library as generic as possible, therefore I want to support floating point and integer types wherever possible.

In order to get the types right, I often use an explicit cast to T in my code. This of course is interpreted as a static_cast in all of my cases. My question therefore is: Do I actually need to static_cast these values? Or may I get runtime overheads by doing/not doing it?

An example:

I currently have functions like these:

template <class T> auto is_elliptic(T eccentricity, T semi_major_axes = T(1)) {
   return T(0) <= eccentricity < T(1) && T(0) < semi_major_axes;
}

However, I could also write it like this:

template <class T> auto is_elliptic(T eccentricity, T semi_major_axes = T(1.0)) {
   return T(0.0) <= eccentricity < T(1.0) && T(0.0) < semi_major_axes;
}

Like this:

template <class T> auto is_elliptic(T eccentricity, T semi_major_axes = 1) {
   return 0 <= eccentricity < 1 && 0 < semi_major_axes;
}

Or like this:

template <class T> auto is_elliptic(T eccentricity, T semi_major_axes = 1.0) {
   return 0.0 <= eccentricity < 1.0 && 0.0 < semi_major_axes;
}

I don't care much for the readability of either version. Neither do I care for the fact, that using an integer type here is probably useless. All I want to know is:

  • Is there a difference between these implementations?
  • If yes, what is it?
  • Plus, are possible optimizations on this code compiler dependent?

Edit:

  • Would any of this change, if I wanted to support custom numerical types?

Edit:

As pointed out in the comments, the chained comparison used above is actually wrong. The code should be something like:

return T(0) <= eccentricity && eccentricity < T(1) && T(0) < semi_major_axes;

Furthermore, the code could be runtime optimized by using a constexpr version:

template <class T> constexpr auto is_elliptic(T eccentricity) {
    return T(0) <= eccentricity && eccentricity < T(1);
}
template <class T> constexpr auto is_elliptic(T eccentricity, T semi_major_axes) {
    return T(0) <= eccentricity && eccentricity < T(1) && T(0) < semi_major_axes;
}
jan.sende
  • 750
  • 6
  • 23
  • 6
    Ouch, chained comparison. 99.99% of the time wrong. – user202729 Jul 26 '18 at 09:22
  • 3
    [Is (4 > y > 1) a valid statement in C++? How do you evaluate it if so? - Stack Overflow](https://stackoverflow.com/questions/8889522/is-4-y-1-a-valid-statement-in-c-how-do-you-evaluate-it-if-so) – user202729 Jul 26 '18 at 09:23
  • Rather than using magic number you can declare some meaningful constants with appropriate type and use them instead of casting every time you need that value. – user7860670 Jul 26 '18 at 09:23
  • 1
    "This of course is interpreted as a `static_cast` in all of my cases." -- What about the cases you didn't consider? What about when `T` is a strongly typed enum, or perhaps a pointer, because someone accidentally passed in the wrong arguments? Do you want to silently accept that, or do you want your compiler to tell you you're doing something wrong? –  Jul 26 '18 at 09:27
  • Well, besides what's already pointed out in comments, I'd also write two (`constexpr`) versions, one with `semi_major_axes` and one without that comparison, instead of a single function with a default parameter. – Bob__ Jul 26 '18 at 09:39
  • Thanks for pointing out the chained comparison! I never really thought about the fact that this could be wrong. Although it's quite clear if one does... – jan.sende Jul 26 '18 at 09:58
  • There is no point to name any of the constants, because they are only valid in certain contexts and not useful for the whole library. Plus, this doesn't help in cases where I want to calculate something like this: `T(1)/semi_major_axes` – jan.sende Jul 26 '18 at 10:03

1 Answers1

1

Depends on what you want.


This answer assumes T is either int or float (although it would work for double, long, or custom types with similar behavior), and x has the type T.

  • If a cast is used:

This is pretty straightforward to understand. However, be careful for cases where the constant is not representible in type T. 0.5>x is not the same as (int)0.5>x.

  • If a cast is not used:

[I]f the promoted operands have different types, additional set of implicit conversions is applied, known as usual arithmetic conversions with the goal to produce the common type [...].

(from https://en.cppreference.com/w/cpp/language/operator_arithmetic )

| T     | Comparison | Equivalent to |
+-------+------------+---------------+
| int   | 0<x        | 0<x           |
| int   | 0.f<x      | 0.f<(float)x  |        (*)
| float | 0<x        | (float)0<x    |
| float | 0.f<x      | 0.f<x         |

For (*), the comparison is done on float, while casting the constant (int(0.f<x)) would make the comparison on integer. For other cases, it's the same.

About compiler overhead: The compilation may be slightly slower, but given that how slow compiling the standard library header is already, it should not matter much.

user202729
  • 3,358
  • 3
  • 25
  • 36
  • Your example makes sense in terms of type safety. In the `(*)` case I get a floating point comparison, although I maybe wanted an integer one. There is probably something in the standard about which type comparison is used in what case. Do you by any chance have a link to that? Plus, I didn't mean compile time overhead, but runtime overhead, because it doesn't really matter to me if the compilation is a bit slower... Can you say something about the runtime cost? – jan.sende Jul 26 '18 at 09:52
  • I found the standard information about type conversion [here](https://en.cppreference.com/w/cpp/language/operator_arithmetic) and [here](https://msdn.microsoft.com/en-us/library/3t4w2bkb.aspx?f=255&MSPPError=-2147217396). However, I'm still not sure about runtime overheads. – jan.sende Jul 28 '18 at 09:33
  • After trying some variants with godbolt, I found that unnecessary conversions might occur. Especially if the source of the constants is somewhat opaque to the compiler. Thus, runtime overhead is possible! – jan.sende Jul 28 '18 at 10:01
  • @jan.sende I'm not sure what you mean by "source of the constants is somewhat opaque to the compiler". What's your code? – user202729 Jul 28 '18 at 10:03
  • If I put the numbers directly into the code, the compiler can optimize the conversion away. However, if I get the number from another source it might not. Compare: `auto test() { return int(1.0); }` with: `auto one() { return 1.0; }` and `auto test() { return int(one()); }` This is the same, but without optimizations you get an unnecessary conversion from `double` to `int` in the second case. (`cvttsd2si` in the assembly) – jan.sende Jul 28 '18 at 10:12
  • @jan.sende Unnecessary? That's completely necessary, as it's explicitly casted, assume compiler doesn't know what `one` is at the time of compiling `test`.. – user202729 Jul 28 '18 at 10:13
  • In a strict sense of course it is necessary. However, in my case it is a constant. Therefore the conversion could be done at compile time. Which is actually the case as soon as you turn optimizations on. – jan.sende Jul 28 '18 at 10:16