In our project, we use quite a lot of "usings" to explicitly state what is variable supposed to represent. It is mainly used for std::string
identificators like PortalId
or CakeId
. Now what we currently can do is
using PortalId = std::string; using CakeId = std::string; PortalId portal_id("2"); CakeId cake_id("is a lie"); portal_id = cake_id; // OK
which we don't like. We would like to have type check during compile time to prevent us from mixing apples and oranges while preserving most of the yum yum methods from the original object.
So the question is - can this be done in C++ such that the usage would be close to what follows, assignments would fail and we could still use it with, say, maps and other containers?
SAFE_TYPEDEF(std::string, PortalId); SAFE_TYPEDEF(std::string, CakeId); int main() { 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; }
We have already tried macros in combination with templates but didn't quite get what we needed. And to add - we CAN use c++17.
EDIT: The code we came up with was
#define SAFE_TYPEDEF(Base, name) \ class name : public Base { \ public: \ template <class... Args> \ explicit name (Args... args) : Base(args...) {} \ const Base& raw() const { return *this; } \ };
which is ugly and doesn't work. And by it doesn't work I mean that compiler was ok with .portal_id = cake_id;
EDIT2: Added explicit
keyword, with which our code actually works nicely for our example. Not sure though whether this is the right way to go and whether it covers all unfortunate situations.
Strong or opaque typedefs are a very powerful feature if you want to prevent errors with the type system – and as I've been advocating for, you want that. Unlike “normal” typedefs, they are a true type definition: they create a new type and allow stuff like overloading on them and/or prevent implicit conversions.
The typedef keyword allows the programmer to create new names for types such as int or, more commonly in C++, templated types--it literally stands for "type definition". Typedefs can be used both to provide more clarity to your code and to make it easier to make changes to the underlying data types that you use.
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; }
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>;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With