10

With a very few exceptions (looking at you, Fahrenheit and Celsius temperature scales), units are linear, and the value zero is simultaneously the additive identity for all units at once.

So given

auto speed = dimensioned(20, _meter/_second);
auto power = dimensioned(75, _watt);

then

if (speed < 6) ...
if (power > 17) ...

makes no more sense than

if (speed > power) ...

you should write

if (speed < dimensioned(6, _mile/_hour)) ...

However, this DOES make sense:

if (speed < 0)

because 0 m/s == 0 mph == 0 A.U./fortnight or any other units that you care to use (for velocity). The question then is how to enable this and only this usage.

C++11 explicit operators and contextual conversion to bool got rid of the need of the "safe-bool" idiom. It appears this problem can be solved with a comparable "safe-zero" idiom:

struct X
{
  int a;
  friend bool operator<(const X& left, const X& right) { return left.a < right.a; }
private:
  struct safe_zero_idiom;
public:
  friend bool operator<(const X& left, safe_zero_idiom*) { return left.a < 0; }
};

Unfortunately it seems that deployed dimension/unit libraries aren't doing this. (This question arose because I actually wanted to test whether a std::chrono::duration was negative). Is this useful? Are there cases that would cause it to fail? Is there any easier way to permit comparison to zero?

One suspects that instead of implementing this for individual operators, there ought to exist an implicit conversion from literal zero to unit-tagged types.


I do note that it allows

 X{1} < nullptr

as a valid expression :(, and unfortunately providing an inaccessible overload of type std::nullptr_t doesn't fix this, since the Standard says in section 4.10

A null pointer constant of integral type can be converted to a prvalue of type std::nullptr_t.

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • 1
    For `speed < 6`, what real harm is there in letting `6` adopt the same units as `speed` implicitly? – jxh Sep 25 '13 at 20:30
  • @jxh: Besides that it would defeat the entire purpose of being unit-safe? Forbidding any implicit association of units is pretty much universal in these libraries, see for example http://stackoverflow.com/a/17733452/103167 – Ben Voigt Sep 25 '13 at 20:33
  • I see, it can prevent a bug in the code if the units for `speed` itself changes. – jxh Sep 25 '13 at 20:36
  • @jxh: Yes, which is especially likely in template code which may work on inputs with different units. – Ben Voigt Sep 25 '13 at 20:37
  • Awsome question. It definitely rings the bell of the universal 0 value that C++ alreay has regarding any type. before nullptr of C++11, NULL was defined to 0. and 0 is assignable to floats and double without conversion warning. 0 has a special universal meaning in C++ I wonder if this is exploitable in your case. – v.oddou Sep 26 '13 at 00:17
  • @v.oddou: I *am* exploiting it here, asking whether this is actually a good idea or I missed some weird case. – Ben Voigt Sep 26 '13 at 03:04
  • @BenVoigt: Do you have some answer to this today finally ? – v.oddou Jul 23 '14 at 10:14
  • Static analysis tools will also complaint about using 0 instead of `nullptr` for pointers. – Mikhail Sep 22 '16 at 20:51

2 Answers2

3
  1. Yes. You convinced me pretty easily that this is useful.
  2. You already pointed out a failure point, for nullptr. I couldn't think of anything other than that.
    My attempts at devising a mechanism to disallow nullptr but allow 0 all yielded complicated schemes that didn't work. Basically, since there is no way to tell C++ you want a constexpr function parameter, it is hard (I won't say impossible yet...) to devise a function that takes an int argument, but results in compile time failure if the argument value is not 0.
  3. If you are okay with allowing nullptr, then an easier implementation would be to use std::nullptr_t directly rather than a separate safe_zero_idiom class. (Admittedly, it is not as safe, since there is no way to access the safe_zero_idiom type in your implementation.)

struct X
{
  int a;
  friend bool operator<(const X& left, const X& right) { return left.a < right.a; }
  friend bool operator<(const X& left, std::nullptr_t) { return left.a < 0; }
};
jxh
  • 69,070
  • 8
  • 110
  • 193
  • Yeah I noticed that. Had another thought, but it turns out `nullptr` converts happily to function and member pointer types as well. – Ben Voigt Sep 26 '13 at 03:49
  • about point 2 : what if you make a template comparison operator, and if the deduced argument is an integral constant, you can static assert against the value 0. – v.oddou Sep 26 '13 at 05:04
  • @v.oddou: I tried that, it didn't work. But if you get it working, please post it. – jxh Sep 26 '13 at 05:07
  • I think you're right, because an actual function parameter cannot be a constexpr, only template parameters can be worked uppon at compile time. – v.oddou Sep 26 '13 at 05:40
  • I think the only feasible solution has to exploit the fact that `nullptr` **is** a `std::nullptr_t`, whereas `0` merely converts to one. Overload resolution can differentiate between the two. Furthermore, `0` converts to `T::*()`, and so does `nullptr`, but almost nothing else does. So, if both overloads exist, the first would be chosen for `nullptr` and the second only for `0`. – MSalters Sep 26 '13 at 10:41
  • @MSalters: The problem is that `0` converts equally well to `nullptr_t` and any pointer type. So it is ambiguous. If you have a working demo I'd love to see it. – Ben Voigt Sep 26 '13 at 14:26
  • @BenVoigt: If I had a solution, I would have posted it as an answer instead of a comment ;) – MSalters Sep 26 '13 at 14:33
0

I could only come up with an obvious solution that drifts away from what you want:

#include <stdexcept>
#include <iostream>
#include <type_traits>
using namespace std;

#include <boost/mpl/int.hpp>

using namespace boost::mpl;

struct X
{
    int a;

    friend bool operator<(const X& left, const X& right)
    {
        return left.a < right.a;
    }

    template< typename T >
    friend bool operator<(const X& left, T zero)
    {
        static_assert( is_same<int_<0>, T>::value, "cannot compare against arbitrary things");
        return left.a < 0;
    }
};

int_<0> unitless0;


int main()
{
    X x;

    //if (x < 3) cout << "oopsie";  // we get a build error here as excpected.

    if (x < unitless0)
        cout << "ok";

    return 0;
}
v.oddou
  • 6,476
  • 3
  • 32
  • 63
  • Oh I definitely wouldn't want that, as it inhibits implicit conversions. Even if a slightly different literal zero were good (which with user-defined literals might not be so bad), `static_assert` is terrible and one should be using SFINAE instead. How is the template helping here anyway, couldn't the type of the second param just be `int_<0>` ? – Ben Voigt Sep 26 '13 at 06:02
  • @BenVoigt : definitely, this is just the result of multi-mutation steps in the making of this code sample. So a hysterical raisin basically :) do not mind it. Also the int_<0> could be something like your safe_zero_type, an empty struct would do the job just perfectly. This code sample is just idiotic. But it serves the discussion. – v.oddou Sep 26 '13 at 08:58