Strongly typed using and typedef
Recently I've encountered a library called NamedTypes which provides nicely wrapped syntactic sugar to do exactly what we needed! Using the library, our example would look like this:
namespace fl = fluent;
using PortalId = fl::NamedType<std::string, struct PortalIdTag>;
using CakeId = fl::NamedType<std::string, struct CakeIdTag, fl::Comparable>;
int main()
{
PortalId portal_id("2");
CakeId cake_id("is a lie");
std::map<CakeId, PortalId> p_to_cake; // OK
p_to_cake.emplace(cake_id, portal_id); // OK
// p_to_cake.emplace(portal_id, cake_id); // COMPILER ERROR
// portal_id = cake_id; // COMPILER ERROR
// portal_id = "1.0"; // COMPILER ERROR
portal_id = PortalId("42"); // OK
return 0;
}
NamedTypes
library provides much more additional properties like Printable
, Incrementable
, Hashable
, etc. which you can use to create e.g. strongly typed indices for arrays and similar. See the linked repository for more details.
Note the usage of .emplace(..)
method, which is necessary because the NamedType
is not default-constructible which is required by the []operator
.
The solutions provided so far seem overly complex so here is my try:
#include <string>
enum string_id {PORTAL, CAKE};
template <int ID> class safe_str : public std::string {
public:
using std::string::string;
};
using PortalId = safe_str<PORTAL>;
using CakeId = safe_str<CAKE>;
Here's a minimal complete solution that will do what you want.
You can add more operators etc to make the class more useful as you see fit.
#include <iostream>
#include <string>
#include <map>
// define some tags to create uniqueness
struct portal_tag {};
struct cake_tag {};
// a string-like identifier that is typed on a tag type
template<class Tag>
struct string_id
{
// needs to be default-constuctable because of use in map[] below
string_id(std::string s) : _value(std::move(s)) {}
string_id() : _value() {}
// provide access to the underlying string value
const std::string& value() const { return _value; }
private:
std::string _value;
// will only compare against same type of id.
friend bool operator < (const string_id& l, const string_id& r) {
return l._value < r._value;
}
};
// create some type aliases for ease of use
using PortalId = string_id<portal_tag>;
using CakeId = string_id<cake_tag>;
using namespace std;
// confirm that requirements are met
auto main() -> int
{
PortalId portal_id("2");
CakeId cake_id("is a lie");
std::map<CakeId, PortalId> p_to_cake; // OK
p_to_cake[cake_id] = portal_id; // OK
// p_to_cake[portal_id] = cake_id; // COMPILER ERROR
// portal_id = cake_id; // COMPILER ERROR
// portal_id = "1.0"; // COMPILER ERROR
portal_id = PortalId("42"); // OK
return 0;
}
here's an updated version that also handles hash maps, streaming to ostream etc.
You will note that I have not provided an operator to convert to string
. This is deliberate. I am requiring that users of this class explicitly express the intent to use it as a string by providing an overload of to_string
.
#include <iostream>
#include <string>
#include <map>
#include <unordered_map>
// define some tags to create uniqueness
struct portal_tag {};
struct cake_tag {};
// a string-like identifier that is typed on a tag type
template<class Tag>
struct string_id
{
using tag_type = Tag;
// needs to be default-constuctable because of use in map[] below
string_id(std::string s) : _value(std::move(s)) {}
string_id() : _value() {}
// provide access to the underlying string value
const std::string& value() const { return _value; }
private:
std::string _value;
// will only compare against same type of id.
friend bool operator < (const string_id& l, const string_id& r) {
return l._value < r._value;
}
friend bool operator == (const string_id& l, const string_id& r) {
return l._value == r._value;
}
// and let's go ahead and provide expected free functions
friend
auto to_string(const string_id& r)
-> const std::string&
{
return r._value;
}
friend
auto operator << (std::ostream& os, const string_id& sid)
-> std::ostream&
{
return os << sid.value();
}
friend
std::size_t hash_code(const string_id& sid)
{
std::size_t seed = typeid(tag_type).hash_code();
seed ^= std::hash<std::string>()(sid._value);
return seed;
}
};
// let's make it hashable
namespace std {
template<class Tag>
struct hash<string_id<Tag>>
{
using argument_type = string_id<Tag>;
using result_type = std::size_t;
result_type operator()(const argument_type& arg) const {
return hash_code(arg);
}
};
}
// create some type aliases for ease of use
using PortalId = string_id<portal_tag>;
using CakeId = string_id<cake_tag>;
using namespace std;
// confirm that requirements are met
auto main() -> int
{
PortalId portal_id("2");
CakeId cake_id("is a lie");
std::map<CakeId, PortalId> p_to_cake; // OK
p_to_cake[cake_id] = portal_id; // OK
// p_to_cake[portal_id] = cake_id; // COMPILER ERROR
// portal_id = cake_id; // COMPILER ERROR
// portal_id = "1.0"; // COMPILER ERROR
portal_id = PortalId("42"); // OK
// extra checks
std::unordered_map<CakeId, PortalId> hashed_ptocake;
hashed_ptocake.emplace(CakeId("foo"), PortalId("bar"));
hashed_ptocake.emplace(CakeId("baz"), PortalId("bar2"));
for(const auto& entry : hashed_ptocake) {
cout << entry.first << " = " << entry.second << '\n';
// exercise string conversion
auto s = to_string(entry.first) + " maps to " + to_string(entry.second);
cout << s << '\n';
}
// if I really want to copy the values of dissimilar types I can express it:
const CakeId cake1("a cake ident");
auto convert = PortalId(to_string(cake1));
cout << "this portal is called '" << convert << "', just like the cake called '" << cake1 << "'\n";
return 0;
}