Wednesday, August 31, 2022

[SOLVED] Creating a type based identification number in old code with minimal changes

Issue

I am maintaining a very old (20+ years) and quite large (15 KLOC) library that has various types of objects which are currently identified by an integer. This poses the issue that just given the integer I cannot know which type of object it should identify. This would be really nice to do at compile time .

The solution I came up with with minimal changes was to create a ID template and then create typedefs for it for the various types of object ID.

I realized I had to add a third param to the template since what I consider two totally different identifiers could have the same underlying type and range.

I found C++ does not consider

typedef int X;
typedef int Y;

As totally different types.

Is this solution :-

A) reasonable (I know it works)

B) Is there another simple way of doing this - Management is scared of high LOC changes

Simplification of solution with only example operator.

#include <iostream>

// Horrible old definition of current code
class OldObjectA
{
   public:
      int ident_; // int identifier
      int uniq_;  // another int identifier unique to OldObjectA's only
};

class OldObjectB
{
   public:
      int ident_;
      int next_; // int of another OldObjectB ident_
      int uniq_; // another int identifier unique to OldObjectB's only
      int dq_;   // int of yet anothera OldObjectB ident_
      int com_;  // int of ident_ of a OldObjectA
      int ld_;   // int of ident_ of a OldObjectC
};

class OldObjectC
{
   public:
      int indent_;
      int next_; // int of another OldObjectC ident_
      int com_;  // int of ident_ of a OldObjectA
};

enum Type { TypeA, TypeAU, TypeB, TypeBU, TypeC };

template<class T, T maxval, Type type>
class ID
{
   public:
      friend bool operator==(const ID<T, maxval, type> &lhs, const ID<T, maxval, type> &rhs)
      {
         std::cout << __PRETTY_FUNCTION__ << std::endl;
         return true;
      }
};

typedef ID<int, 42, TypeA>  ID_A;
typedef ID<int, 42, TypeAU> ID_UniqA;
typedef ID<int, 42, TypeB>  ID_B;
typedef ID<int, 42, TypeBU> ID_UniqB;
typedef ID<int, 100, TypeC> ID_C;

// What I was thinking of doing
class NewObjectA
{
   public:
      ID_A ident_; // int identifier
      ID_UniqA uniq_;  // another int identifer
};

class NewObjectB
{
   public:
      ID_B ident_;
      ID_B next_; // int of another OldObjectB ident_
      ID_UniqB uniq_; // another int
      ID_B dq_;   // int of yet anothera OldObjectB ident_
      ID_A com_;  // int of ident_ of a OldObjectA
      ID_C ld_;   // int of ident_ of a OldObjectC
};

class NewObjectC
{
   public:
      ID_C indent_;
      ID_C next_; // int of another OldObjectC ident_
      ID_A com_;  // int of ident_ of a OldObjectA
};

int main(int argc, char *argv[])
{
   std::cout << "================================================================================\n";
   ID_A a,a2;
   ID_UniqA au,au2;
   ID_B b,b2;
   ID_UniqB bu,bu2;
   ID_C c,c2;

   a==a2;
   au==au2;
   b==b2;
   bu==bu2;
   c==c2;

   // wanted and expected compile time fails
   // a=au;
   // a=b;
   // a=bu;
   // a=c;
   // au=b;
   // au=bu;
   // au=c;
   // b=bu;
   // b=c;

   std::cout << "================================================================================\n";


  return 0;
}

Solution

The idea of adding template arguments to differentiate between otherwise identical types is reasonable. It is a useful technique that crops up once every so often. Most recently I used a similar technique in defining types for measurements (ie km, litres, seconds and so on).

There is at least one simplification that you can make to what you have. The enum is not necessary. You can get away with using the types themselves in their ID definitions.

template<class T, T maxval, class tag>
struct ID {
};

template<class T, T maxval, class tag>
bool operator==(ID<T, maxval, tag> lhs, ID<T, maxval, tag> rhs)
{
    return true;
}

typedef ID<int, 42, class NewObjectA> ID_A;
typedef ID<int, 42, class NewObjectB> ID_B;

struct NewObjectA {
    ID_A id;
};

struct NewObjectB {
    ID_B id;
};

void f()
{
    ID_A id_a;
    ID_B id_b;

    id_a == id_a;
    id_b == id_b;
    //id_a == id_b; // won't compile as expected
}

This has the advantage of not creating a global list of all types in your program in a single place. Using the types themselves may also make things easier if you need to deal IDs in line with conversions permitted by the inheritance hierarchy. For example, ObjectC is a ObjectA so ID_C is convertible to ID_A.

I find with adding this sort of thing (and the measurements stuff I am doing is similar) that incremental approach is generally the way to go. Each time you need to touch a piece of code introduce some improvements locally and confirm they work as expected before making more changes. Testing the changes is especially important if you think you have identified a bug and changed program behaviour. Trying to change everything all at once in contrast often ends up being a lot of pain.



Answered By - Bowie Owens
Answer Checked By - Timothy Miller (WPSolving Admin)