Reputation: 161
Suppose we have two types which are basically int
s:
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: 323
Reputation: 38209
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
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