Unique Integer Types -- a useful C++ trick

I am currently working on a cache simulator, and found that it was using variables of type "unsigned" to mean a zillion different things. Unsigned values were used as data addresses, cache tags, entry numbers, set numbers, physical cpu numbers, virtual cpu numbers, offsets... In the course of working on this, it was very easy for me to make a mistake where I used a value when I really should have converted it somehow before using it in that way. What I really needed was for the compiler to give me different types of unsigned value.

My first attempt used typedef, and did something like this:

typedef unsigned set_num_t;
typedef unsigned entry_num_t;
typedef unsigned lru_entry_t;
typedef unsigned tag_t;
typedef unsigned etag_t;
This gave me the ability to use different types to mean different things, but the compiler didn't stop me from accidentally using one type where I really meant to use another! What I really needed was a way of differentiating the types.

With the help of my friends on zephyr, I came up with the following class to use instead:

template <int unique_type_id>
class unique_int {
  unsigned val_;

  // Do not define this function -- we want to prevent constructing a
  // unique_int from a bool:
  unique_int(bool v);

public:
  static unique_int INVALID() { return unique_int(); }

  unique_int(unsigned v = ~0u) : val_(v) {}

  // Note, I am not defining every operator here, just the ones I
  // actually have needed to use...
  bool operator==(const unique_int &b) const { return val_ == b.val_; }
  bool operator!=(const unique_int &b) const { return val_ != b.val_; }
  bool operator<(const unique_int &b) const { return val_ < b.val_; }
  bool operator>(const unique_int &b) const { return val_ > b.val_; }
  bool operator<=(const unique_int &b) const { return val_ >= b.val_; }
  bool operator>=(const unique_int &b) const { return val_ >= b.val_; }
  unique_int operator+(const unique_int &b) const
    { return unique_int(val_ + b.val_); }
  unique_int operator-(const unique_int &b) const
    { return unique_int(val_ - b.val_); }
  unique_int operator*(const unique_int &b) const
    { return unique_int(val_ * b.val_); }
  unique_int operator/(const unique_int &b) const
    { return unique_int(val_ / b.val_); }
  unique_int operator%(const unique_int &b) const
    { return unique_int(val_ % b.val_); }
  unique_int operator++() {
    return unique_int(++val_);
  }
  unique_int operator++(int) {
    return unique_int(val_++);
  }
  unique_int operator--() { val_--; return *this; }

  // Note: This gets rid of all of the type safety associated with
  // using this!  You should try to only use this when you have a
  // debug printf():
  unsigned val() const { return val_; }
};
Now I could define my types like this:
typedef unique_int<0> set_num_t;
typedef unique_int<1> entry_num_t;
typedef unique_int<2> lru_entry_t;
typedef unique_int<3> tag_t;
typedef unique_int<4> etag_t;
And the compiler now stopped me from accidentally using one type where I meant to use another type!

I was left with only one problem --- I couldn't use these new types as indexes into arrays. This was solved by creating a new array type, which also has the bonus feature of giving you an easy place to put array bounds checking:

template <class index_type,
	  class type>
class typed_array {
  type *array_;
  index_type size_;

  // I would really rather that arrays just don't get implicitly
  // copied.  If you want to pass an array around, pass around a
  // reference or a pointer to it.  If you want to make a copy, then
  // use operator=:
  typed_array(const typed_array &no_copies_allowed);

public:
  typed_array() :
    array_(NULL),
    size_(0u)
    {}

  ~typed_array() {
    delete[] array_;
  }

  void setup(index_type size) {
    delete array_;
    if(size > 0u) {
      array_ = new type[size.val()];
    }
    size_ = size;	
  }

  typed_array& operator=(const typed_array &other) {
    setup(other.size_);
    for(index_type i = 0u; i < size_; i++) {
      array_[i.val()] = other.array_[i.val()];
    }
    return *this;
  }

  type& operator[](index_type index) const {
    assert(index < size_);
    return array_[index.val()];
  }
};
You can use one of these arrays like this:
typed_array<set_num_t, typed_array<entry_num_t, unsigned> > cache;

// Initialize the cache to be 4 sets and 2 entries per set:
cache.setup(4);
for(set_num_t i = 0u; i < 4u; i++) {
    cache[i].setup(2);
}

set_num_t set(0u);
entry_num_t entry(0u);

// This is correct, and the compiler allows this:
cache[set][entry] = 5;

// This is backwards, and the compiler will generate an error:
cache[entry][set] = 6;

A couple of notes:


Christopher Brian Colohan
Last modified: Fri Nov 19 15:35:58 EST 2004