Zamfir Yonchev
Zamfir Yonchev

Reputation: 161

What is the best way to tell the compiler to differentiate between two types which hold the same kind of data

Suppose we have two types which are basically ints:

alias UserID = int;
alias ItemID = int;

And suppose we have two functions which take arguments of the two types:

void add_user(UserID id) { std::cout << "Adding user with id " << id << '\n'; }
void add_item(ItemID id) { std::cout << "Adding item with id " << id << '\n'; }

The problem is that we can do:

UserID uid {5};
add_item(uid);

And the compiler will not be able to protect me from passing the wrong type because both types alias the same underlying type.

What is the least boilerplate I need to add to make the compiler differentiate between the two types?

My current solution is to define the types as structs like this:

struct UserID
{
  int val;
  operator int() { return val; }
};

struct ItemID
{
  int val;
  operator int() { return val; }
};

The operator int() is to allow for an implicit conversion to the base type.

One problem with this approach is that sometimes I need to do arithmetic operations on the types, like for example:

UserID uid {5};
++uid;

Another problem is when these types are passed to a template and a template-argument deduction takes place where the correct type to deduce is int but the compiler is not able to see through the conversion operator.

I'm wondering if there's a more elegant solution where both types would seamlessly convert to their base type when the base type is required but would still be different when the wrapping type is required. What I want is the following:

add_user(UserID{1}); //compiles
add_item(ItemID{2}); //compiles
add_user(ItemID{3}); //doesn't compile
UserID uid = UserID{4} + 1; //compiles
++uid; //compiles
UserID uid2 = ItemID{5} + 1; //doesn't compile

By 'the least boilerplate' I mean the total boilerplate code I need to write both in the definition of the types as well as in the user code.

Upvotes: 0

Views: 322

Answers (2)

Marek R
Marek R

Reputation: 38181

You should use some ready library, here is an example of NamedType:

#include <NamedType/named_type.hpp>
#include <iostream>

using UserID = fluent::NamedType<int, struct UserIDTag, fluent::PreIncrementable, fluent::Addable, fluent::Printable>;
using ItemID = fluent::NamedType<int, struct ItemIDTag, fluent::PreIncrementable, fluent::Addable, fluent::Printable>;

void add_user(UserID id) { std::cout << "Adding user with id " << id << '\n'; }
void add_item(ItemID id) { std::cout << "Adding item with id " << id << '\n'; }

int main()
{
    UserID uid { 5 };
    // add_item(uid); // fails to compile
    add_user(uid);

    add_user(UserID { 1 }); // compiles
    add_item(ItemID { 2 }); // compiles
    // add_user(ItemID { 3 }); // doesn't compile
    // UserID uid2 = UserID { 4 } + 1; // should compile - feature not supported
    ++uid; // compiles
    // UserID uid2 = ItemID { 5 } + 1; // doesn't compile

    return 0;
}

Live demo. Cool godbolt supports that library :).

Upvotes: 1

molbdnilo
molbdnilo

Reputation: 66459

You can use "shadow types" (they have names but no definition) and a template (or use some "strong typedef" library):

template <typename>
class Id
{
    int val;
    operator int() const { return val; }
};

using UserID = Id<struct user_tag_>;
using ItemID = Id<struct item_tag_>;

And you should provide operator overloads for the operations that make sense - you most likely don't want to divide or multiply these IDs by anything.

Upvotes: 6

Related Questions