Strong typing identifier with templates - inheritance or tag?

Started by
13 comments, last by Zipster 6 years, 2 months ago

I'm working a lot with identifiers types to help me while debugging or managing collections of instances in my code. Previously, I had using directives as the following example code:


using AnimalId = unsigned int;
using TreeId = unsigned int;

 

After a while, I accidentally mixed AnimalId and TreeId in my logic, so I've decided to use structs in order to make the identifiers strong typed. To avoid code duplications, I've created a template


template <typename TypeTag, typename ValueType>
struct IdType
{
	ValueType value;
	// Methods for comparison and hashes
};

 

However, I believe there's two different ways of using this template:


// Using directive
struct AnimalIdTag {}
using AnimalId = IdType<AnimalIdTag, unsigned int>;
  
// Inheritance
struct TreeId : IdType<TreeId, unsigned int>;

 

And here is where I got in doubt. It seems both ways are valid, but there should be some differences. For example, with inheritance I can forward declare the TreeId on headers, which doesn't seem really feasible (in a painless way) with AnimalId. However, TreeId uses inheritance, and my knowledge on how inheritance and templates works "in background" is too weak to say, but it feels like there might be some hidden drawback.

Are there differences to make deciding which one to use easier? Or there's currently no drawbacks (besides being able to forward declare or not)?

Advertisement

Are there differences in terms of the efficiency of generated code?  Probably not.

Are there differences in terms of meaning?  Definitely.

You inherit to be reused.  Unless you plan on using your Ids through a pointer or reference to their common base class (and it's not clear why you'd ever want to do that, since weak typing was the root of your troubles to start with), you should stay away from using inheritance and stick with the type alias.

Note that instead of using struct AnimalIdTag{} you could also use an enumeration or strongly-typed enumeration (an enum or an enum class) or set of constexpr constants and a non-type template parameter.  Again, it won't make any difference in terms of code efficiency, but it might be better in terms of code clarity.

enum class ObjectType {
  Animal,
  Tree,
};

template<ObjectType T, typename ValueType = std::uint32_t>
  struct IdType
  {
    ValueType value;
    // member functions as desired
  };

using AnimalId = IdType<ObjectType::Animal>;

 

Stephen M. Webb
Professional Free Software Developer

1 hour ago, Bregma said:

You inherit to be reused.  Unless you plan on using your Ids through a pointer or reference to their common base class (and it's not clear why you'd ever want to do that, since weak typing was the root of your troubles to start with), you should stay away from using inheritance and stick with the type alias.

Note that instead of using struct AnimalIdTag{} you could also use an enumeration or strongly-typed enumeration (an enum or an enum class) or set of constexpr constants and a non-type template parameter.  Again, it won't make any difference in terms of code efficiency, but it might be better in terms of code clarity.

 

Awesome advice! Clarity of intention was something that I hadn't considered, and it'll definitely help a lot - specially on the long run! The enum idea seems like a good catch. I'll check how it'll fit into the code!

4 hours ago, Bregma said:

Note that instead of using struct AnimalIdTag{} you could also use an enumeration or strongly-typed enumeration (an enum or an enum class) or set of constexpr constants and a non-type template parameter.  Again, it won't make any difference in terms of code efficiency, but it might be better in terms of code clarity.

Why would that be better? Surely it would mean that every time you want to add a new type, you need to edit the enum?

Personally, having to edit the definition of a type just to use it feels like a code smell to me.

 

 

if you think programming is like sex, you probably haven't done much of either.-------------- - capn_midnight
1 hour ago, ChaosEngine said:

Why would that be better? Surely it would mean that every time you want to add a new type, you need to edit the enum?

Personally, having to edit the definition of a type just to use it feels like a code smell to me.

Not only that, but I find this strong identifier pattern to be infinitely more useful when expressed using trait types:


struct TreeIdTraits
{
  using underlying_type = std::string;
  using reference_type = Tree*;
  
  static underlying_type from_instance(const reference_type instance)
  {
    return (instance != nullptr) ? instance->get_id() : Tree::InvalidId;
  }
  
  static reference_type to_instance(underlying_type id)
  {
    return get_tree_by_id(id);
  }
};

struct AnimalIdTraits
{
  using underlying_type = std::uint32_t;
  using reference_type = std::shared_ptr<Animal>;
  
  static underlying_type from_instance(const reference_type instance)
  {
    return instance ? instance->get_id() : Animal::InvalidId;
  }
  
  static reference_type to_instance(underlying_type id)
  {
    return get_animal_by_id(id);
  }
};

template <typename IdTraits>
struct IdType
{
  using underlying_type = typename IdTraits::underlying_type;
  using reference_type = typename IdTraits::reference_type;

  explicit IdType(underlying_type value) : id(value) {}
  explicit IdType(const reference_type instance) : id(IdTraits::from_instance(instance)) {}
  
  reference_type get() const
  {
    return IdTraits::to_instance(id);
  }
  
  operator bool() const
  {
    return static_cast<bool>(get());
  }

private:
  underlying_type id;
};

using TreeId = IdType<TreeIdTraits>;
using AnimalId = IdType<AnimalIdTraits>;

I chose to add some "weak reference" behavior to demonstrate just how easily you can incorporate practical functionality into these identifiers beyond just a compile error if you accidentally mix them up.

3 hours ago, ChaosEngine said:

Why would that be better? Surely it would mean that every time you want to add a new type, you need to edit the enum?

Personally, having to edit the definition of a type just to use it feels like a code smell to me.

It isnt. We use the same technics in our commercial software to clearly differ for Loca-Ids, Resource-Ids, Manager-Ids ... to have some kind of debugging and error handling inside the code. You cant for example call a loka string from a resource id and this will give your public API a more clear access as a simple numerical id could.

The question should be if the code this kind of structure is intended to is sealed or part of a framework (not API, thats a difference). In the sealed way, you have to change the enum while you are in development what is part of code evolution and mostly you are not really able to predict what types your enum will have at the end. Also you will have a finite list of types that run through this id struct in opposite to the public framework case

In the public framework case, you might be right but as long as we dont knoe about that, we assume the sealed code case. In dubio pro reo.

But I like the traits solution

16 hours ago, Shaarigan said:

In the public framework case, you might be right but as long as we dont knoe about that, we assume the sealed code case. In dubio pro reo.

In a public framework, the enum would be completely out of the question, so I had assumed "sealed code".

I'm still interested to hear why @Bregma thinks it's a better solution than a tag. 

if you think programming is like sex, you probably haven't done much of either.-------------- - capn_midnight

Thanks for the discussion!

On 1/16/2018 at 2:26 AM, Zipster said:

Not only that, but I find this strong identifier pattern to be infinitely more useful when expressed using trait types

I liked the way that the type traits solution help me in expanding type information (if needed) without adding more template arguments. I got into a small problem with include hell so I left out the reference type for now (need to change some design to use it...), but I'm going with this solution. Thanks for the clear code example!

 

On 1/16/2018 at 5:23 AM, Shaarigan said:

The question should be if the code this kind of structure is intended to is sealed or part of a framework (not API, thats a difference).

 

10 hours ago, ChaosEngine said:

In a public framework, the enum would be completely out of the question, so I had assumed "sealed code".

 

The generic identifier in my case, is meant to be used over different components and systems on-demand, but on some it is possible to say up-front groups of id types that are unlikely to change (i.e RendererId, TextureId). For these, enum seemed attractive - but I decided to not use them in order to use the same code for situations where enums don't fit. It didn't come to me that the bigger context of how it'd be used would affect the suggestions. I'll keep that in mind for other questions :}

Just one more suggestion.  If you're using a compiler that supports the current C++ standard (and is not limited to an old superseded version of the language) you can just use a fixed enum type as your index type.


#include <cstdint>

enum AnimalId: std::uint32_t;
enum TreeId: std::uint32_t;

void f(AnimalId)
{ }


int
main()
{
  AnimalId aid{1000};
  TreeId tid{42};

//  tid = aid; -- will fail to compile because the types are different
  f(aid); // -- OK
//  f(tid); -- will fail to compile because there is no valid conversion
}

 

Stephen M. Webb
Professional Free Software Developer

16 hours ago, ChaosEngine said:

In a public framework, the enum would be completely out of the question, so I had assumed "sealed code".

I'm still interested to hear why @Bregma thinks it's a better solution than a tag. 

I'm not Bregma, but one thing that comes to mind is that it allows you use the enum value as "type code" at runtime. That has a number of use cases, but the one that comes to mind is cases where you convert between ID types and you want to validate at runtime that your ID points at what you think it's pointing at. For example, you might have an object hierarchy where Animal and Tree both inherit from Actor; you have ActorID, AnimalID, and TreeID. AnimalID and TreeID can be converted to ActorID and vice versa. Now you have a potential case where an ActorID that actually points at an Animal could be converted to a TreeID, which you probably want to validate at runtime! You don't strictly NEED to use an enum for the type code, but it simplifies the problem of having unique IDs per type. Especially in an environment where RTTI is disabled.

This article illustrates an example of how to set up a handle type that uses "indices with type code" handles: http://gamesfromwithin.com/managing-data-relationships

Another thought that comes to mind is that you might want to use the "type code" itself as an index or key. For example, suppose you have some content that specifies a table mapping object type onto some parameter that's uniform across all objects of those types, because the designers don't want to have to set that parameter on every single object definition of that type in content. If you have a type code that's mappable to a simple array index, you can have a flat array in the content and just index into that to get the value you're looking for.

Another option that hasn't been mentioned is specializing your handle type on the actual type it's supposed to refer to, eg.


struct Animal
{
  // ...
};

using AnimalID = IDType<Animal, unsigned int>;

This obviates the need for an extraneous type that never gets used just for the tag.

This topic is closed to new replies.

Advertisement