53

I've been trying to think of a way of declaring strongly typed typedefs, to catch a certain class of bugs in the compilation stage. It's often the case that I'll typedef an int into several types of ids, or a vector to position or velocity:

typedef int EntityID;
typedef int ModelID;
typedef Vector3 Position;
typedef Vector3 Velocity;

This can make the intent of code more clear, but after a long night of coding one might make silly mistakes like comparing different kinds of ids, or adding a position to a velocity perhaps.

EntityID eID;
ModelID mID;

if ( eID == mID ) // <- Compiler sees nothing wrong
{ /*bug*/ }


Position p;
Velocity v;

Position newP = p + v; // bug, meant p + v*s but compiler sees nothing wrong

Unfortunately, suggestions I've found for strongly typed typedefs include using boost, which at least for me isn't a possibility (I do have c++11 at least). So after a bit of thinking, I came upon this idea, and wanted to run it by someone.

First, you declare the base type as a template. The template parameter isn't used for anything in the definition, however:

template < typename T >
class IDType
{
    unsigned int m_id;

    public:
        IDType( unsigned int const& i_id ): m_id {i_id} {};
        friend bool operator==<T>( IDType<T> const& i_lhs, IDType<T> const& i_rhs );
};

Friend functions actually need to be forward declared before the class definition, which requires a forward declaration of the template class.

We then define all the members for the base type, just remembering that it's a template class.

Finally, when we want to use it, we typedef it as:

class EntityT;
typedef IDType<EntityT> EntityID;
class ModelT;
typedef IDType<ModelT> ModelID;

The types are now entirely separate. Functions that take an EntityID will throw a compiler error if you try to feed them a ModelID instead, for example. Aside from having to declare the base types as templates, with the issues that entails, it's also fairly compact.

I was hoping anyone had comments or critiques about this idea?

One issue that came to mind while writing this, in the case of positions and velocities for example, would be that I can't convert between types as freely as before. Where before multiplying a vector by a scalar would give another vector, so I could do:

typedef float Time;
typedef Vector3 Position;
typedef Vector3 Velocity;

Time t = 1.0f;
Position p = { 0.0f };
Velocity v = { 1.0f, 0.0f, 0.0f };

Position newP = p + v*t;

With my strongly typed typedef I'd have to tell the compiler that multypling a Velocity by a Time results in a Position.

class TimeT;
typedef Float<TimeT> Time;
class PositionT;
typedef Vector3<PositionT> Position;
class VelocityT;
typedef Vector3<VelocityT> Velocity;

Time t = 1.0f;
Position p = { 0.0f };
Velocity v = { 1.0f, 0.0f, 0.0f };

Position newP = p + v*t; // Compiler error

To solve this, I think I'd have to specialize every conversion explicitly, which can be kind of a bother. On the other hand, this limitation can help prevent other kinds of errors (say, multiplying a Velocity by a Distance, perhaps, which wouldn't make sense in this domain). So I'm torn, and wondering if people have any opinions on my original issue, or my approach to solving it.

2

3 Answers 3

45

These are phantom type parameters, that is, parameters of a parameterised type that are used not for their representation, but to separate different “spaces” of types with the same representation.

And speaking of spaces, that’s a useful application of phantom types:

template<typename Space>
struct Point { double x, y; };

struct WorldSpace;
struct ScreenSpace;

// Conversions between coordinate spaces are explicit.
Point<ScreenSpace> project(Point<WorldSpace> p, const Camera& c) { … }

As you’ve seen, though, there are some difficulties with unit types. One thing you can do is decompose units into a vector of integer exponents on the fundamental components:

template<typename T, int Meters, int Seconds>
struct Unit {
  Unit(const T& value) : value(value) {}
  T value;
};

template<typename T, int MA, int MB, int SA, int SB>
Unit<T, MA - MB, SA - SB>
operator/(const Unit<T, MA, SA>& a, const Unit<T, MB, SB>& b) {
  return a.value / b.value;
}

Unit<double, 0, 0> one(1);
Unit<double, 1, 0> one_meter(1);
Unit<double, 0, 1> one_second(1);

// Unit<double, 1, -1>
auto one_meter_per_second = one_meter / one_second;

Here we’re using phantom values to tag runtime values with compile-time information about the exponents on the units involved. This scales better than making separate structures for velocities, distances, and so on, and might be enough to cover your use case.

2
  • 3
    Hmm, using the template system for enforcing units on operations is cool. Hadn't thought of it, thanks! Now I'm wondering if you can enforce things like conversions between meter and kilometer, for example.
    – Kian
    Commented Jun 6, 2014 at 13:22
  • @Kian: Presumably you would use the SI base units internally—m, kg, s, A, &c.—and just define an alias 1km = 1000m for convenience.
    – Jon Purdy
    Commented Jun 9, 2014 at 9:52
7

I had a similar case where I wanted to distinguish different meanings of some integer values, and forbid implicit conversions between them. I wrote a generic class like this:

template <typename T, typename Meaning>
struct Explicit
{
  //! Default constructor does not initialize the value.
  Explicit()
  { }

  //! Construction from a fundamental value.
  Explicit(T value)
    : value(value)
  { }

  //! Implicit conversion back to the fundamental data type.
  inline operator T () const { return value; }

  //! The actual fundamental value.
  T value;
};

Of course if you want to be even more safe, you can make the T constructor explicit as well. The Meaning is then used like this:

typedef Explicit<int, struct EntityIDTag> EntityID;
typedef Explicit<int, struct ModelIDTag> ModelID;
4
  • 1
    This is interesting, but I'm not sure it's quite strong enough. It will ensure that if I declare a function with the typedefed type, only the right elements can be used as parameters, which is good. But for every other use it adds syntactic overhead without preventing mixing of parameters. Say operations such as comparing. operator==(int, int) will take an EntityID and a ModelID without complaint (even if explicit requires that I cast it, it doesn't keep me from using the wrong variables).
    – Kian
    Commented Jun 6, 2014 at 13:18
  • Yes. In my case I had to prevent myself from assigning different sorts of IDs to one another. Comparisons and arithmetic operations were not my main concern. The above construct will prohibit assignment, but not other operations.
    – mindriot
    Commented Jun 6, 2014 at 14:55
  • If you're willing to put more energy into this, you can build a (fairly) generic version that handles operators as well, by making the Explicit class wrap the most common operators. See pastebin.com/FQDuAXdu for an example - you need some fairly complex SFINAE constructs to determine whether the wrapper class actually provides the wrapped operators or not (see this SO question). Mind you, it still can't cover all cases and may not be worth the trouble.
    – mindriot
    Commented Jun 6, 2014 at 16:03
  • While syntactically elegant, this solution will incur significant performance penalty for integer-types. Integers can be passed via registers, structs (even containing a single integer) can not.
    – Ghostrider
    Commented Apr 25, 2016 at 20:56
1

I'm not sure how the following works out in production code (I'm a C++/programming beginner, like, CS101 beginner), but I cooked this up using C++'s macro sys.

#define newtype(type_, type_alias) struct type_alias { \

/* make a new struct type with one value field
of a specified type (could be another struct with appropriate `=` operator*/

    type_ inner_public_field_thing; \  // the masked_value
    \
    explicit type_alias( type_ new_value ) { \  // the casting through a constructor
    // not sure how this'll work when casting non-const values
    // (like `type_alias(variable)` as opposed to `type_alias(bare_value)`
        inner_public_field_thing = new_value; } }
2
  • Note: Please let me know of any pitfalls/improvements you think of.
    – Noein
    Commented May 20, 2015 at 8:11
  • 1
    Can you add some code showing how this macro is used -- like on the examples in the original question? If so, this is a great answer.
    – Jay Elston
    Commented May 21, 2015 at 0:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.