0

I'd like to add compile-time checking for different meanings of double. In the real world, I'm trying to make sure all calculations are done in consistent units. For the purposes of this question, I've concocted a toy example in which numbers have flavors.

I've been trying to achieve this based on a template parameter. Using the c++0x aliases feature described in another answer, I declared a Number<Flavor> as:

enum Flavor { Cherry, Plum, Raspberry };

template <Flavor> using Number = double;

This gives me the ability to declare local variables or parameters as particular flavors of Number, then use those variables as ordinary doubles in most contexts.

My problem is, I can't find a way to declare a function that will only accept a particular flavor as its argument:

void printCherryNumber(Number<Cherry> num) { cout << num << endl; }

int main() {
    Number<Cherry> a(5);
    Number<Plum> b(6);
    Number<Raspberry> c(3.1415);

    printCherryNumber(a);
    printCherryNumber(b);  // O, if only this could be a compiler error.

    return 0;
}

My goal is to make printCherryNumber(b) fail to compile because b is a Number<Plum> not Number<Cherry>. Many existing questions tackle variations on this problem with solutions that seem to not work on the type alias construct I've used for Number.

Stuff I've Tried

From this answer, I see the suggestion to add a templated version of the function that explicitly does nothing or breaks, as in

template <typename T> void printCherryNumber(T num) = delete;

This has no effect at all, and why should it? Number<Plum> is really double and Number<Cherry> is also double so the compiler never bothers with the templated version.

Another answer suggests using a single templated function and static asserts, as in:

template <Flavor F> void printPlumNumber(Number<F> num) {
    static_assert(F == Plum, "Wrong number flavor!");
    cout << num << endl;
}

This fails because regardless of the actual value of F, Number<F> is still just double and so I get an error about not being able to infer the value of F.

Elsewhere someone suggests explicit specialization, which also fails for this case:

template <Flavor F> void printRaspberryNumber(Number<F> num) = delete;

template <> void printRaspberryNumber<Raspberry>(Number<Raspberry> num) {
    cout << num << endl;
}

Here, the compiler treats the call as ambiguous, in part again because it can't infer a value for F.

Elephant in the Room

I could, of course, make Number a single-value struct in the form of

template <Flavor> struct Number { double value; };

but I'm trying to avoid this option because I'm not terribly thrilled about the idea of having .value all over everywhere in my code, nor am I especially eager to define operators for Number that just proxy down to double.

Obligatory ideone

http://ideone.com/4HiYtI

Community
  • 1
  • 1
Dan
  • 4,312
  • 16
  • 28

2 Answers2

2

The problem with this approach:

enum Flavor { Cherry, Plum, Raspberry };

template <Flavor> using Number = double;

is that alias templates are transparent. Number<Cherry>, Number<Plum>, and double are all the same type. That doesn't solve your problem at all.


What you want is typically called an opaque typedef. You really do want your last option:

template <Flavor>
struct Number {
    double value;

    operator double() const { return value; } // for convenience
    Number& operator=(double ); // if necessary
    // possibly more operations
};

This way, Number<Cherry> and Number<Plum> are different types. They are not convertible to each other. And double is not implicitly convertible to either.


You can also take a look at BOOST_STRONG_TYPEDEF and its implementation, it's intended to solve this problem too.

Barry
  • 286,269
  • 29
  • 621
  • 977
1

The option you're trying to avoid is really the only way to do this.

A template alias is what it is: an alias. A template alias is equivalent to the underlying type. In all respects.

template <Flavor> using Number = double;

This means that Number<Flavor> is a double. It is not anything else. Number<Plum> is a double, too. This is pretty much the same as doing a global search/replace of either one of them to double. The end result will be identical. The type is exactly the same.

You can only "declare a function that will only accept" a specific type. Except using a template alias, the template alias is the same type, as such it is not possible to declare a function that accept a double, but does not accept a double. It is a logical falsity.

Wrapping a double in a struct is the only way to achieve strict type checking, of this kind. It's not that bad. Toss in a few overloads, a few operators, and your wrapped struct will enforce strict type checking, and the compiler is likely to produce identical code, with no runtime penalty.

Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148