Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Strongly typed using and typedef

Tags:

c++

c++17

c++14

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.

like image 632
Jendas Avatar asked Dec 15 '15 11:12

Jendas


People also ask

What is a strong typedef?

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.

When should you use typedef?

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.


2 Answers

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; } 
like image 186
Richard Hodges Avatar answered Oct 12 '22 00:10

Richard Hodges


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>; 
like image 22
kamikaze Avatar answered Oct 11 '22 22:10

kamikaze