Jump to content
  • Advertisement
Sign in to follow this  
Kirlim

C++ Strong typing identifier with templates - inheritance or tag?

Recommended Posts

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)?

Edited by Kirlim
Fixed code example

Share this post


Link to post
Share on other sites
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>;

 

Share this post


Link to post
Share on other sites
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!

Share this post


Link to post
Share on other sites
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.

 

 

Share this post


Link to post
Share on other sites
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.

Edited by Zipster

Share this post


Link to post
Share on other sites
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

Edited by Shaarigan

Share this post


Link to post
Share on other sites
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. 

Share this post


Link to post
Share on other sites

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 :}

Share this post


Link to post
Share on other sites

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
}

 

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!