Strongly typed using and typedef

C++C++14C++17

C++ Problem Overview


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.

C++ Solutions


Solution 1 - C++

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;
}

Solution 2 - C++

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>;

Solution 3 - C++

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.

Solution 4 - C++

It would be nice if there were a standard way to do this, but currently there isn't. Something might be standardised in the future: there is a paper on Opaque Typedefs which tries to do this with function alias and a richer inheritance construct and one on Named Types which takes a much simpler approach with a single new keyword to introduce a strong typedef, or whatever you want to call it.

The Boost Serialization library supplies BOOST_STRONG_TYPEDEF which might give you what you want.

Here's a drop-in replacement for your SAFE_TYPEDEF which is just BOOST_STRONG_TYPEDEF with no other boost dependencies and modified slightly so that you can't assign from the typedefd type. I also added a move constructor and assignment operator and made use of default:

namespace detail {
    template <typename T> class empty_base {};
}

template <class T, class U, class B = ::detail::empty_base<T> >
struct less_than_comparable2 : B
{
     friend bool operator<=(const T& x, const U& y) { return !(x > y); }
     friend bool operator>=(const T& x, const U& y) { return !(x < y); }
     friend bool operator>(const U& x, const T& y)  { return y < x; }
     friend bool operator<(const U& x, const T& y)  { return y > x; }
     friend bool operator<=(const U& x, const T& y) { return !(y < x); }
     friend bool operator>=(const U& x, const T& y) { return !(y > x); }
};

template <class T, class B = ::detail::empty_base<T> >
struct less_than_comparable1 : B
{
     friend bool operator>(const T& x, const T& y)  { return y < x; }
     friend bool operator<=(const T& x, const T& y) { return !(y < x); }
     friend bool operator>=(const T& x, const T& y) { return !(x < y); }
};

template <class T, class U, class B = ::detail::empty_base<T> >
struct equality_comparable2 : B
{
     friend bool operator==(const U& y, const T& x) { return x == y; }
     friend bool operator!=(const U& y, const T& x) { return !(x == y); }
     friend bool operator!=(const T& y, const U& x) { return !(y == x); }
};

template <class T, class B = ::detail::empty_base<T> >
struct equality_comparable1 : B
{
     friend bool operator!=(const T& x, const T& y) { return !(x == y); }
};

template <class T, class U, class B = ::detail::empty_base<T> >
struct totally_ordered2
    : less_than_comparable2<T, U
    , equality_comparable2<T, U, B
      > > {};

template <class T, class B = ::detail::empty_base<T> >
struct totally_ordered1
    : less_than_comparable1<T
    , equality_comparable1<T, B
      > > {};

#define SAFE_TYPEDEF(T, D)                                      \
struct D                                                        \
    : totally_ordered1< D                                       \
    , totally_ordered2< D, T                                    \
    > >                                                         \
{                                                               \
    T t;                                                        \
    explicit D(const T& t_) : t(t_) {};                         \
    explicit D(T&& t_) : t(std::move(t_)) {};                   \ 
    D() = default;                                              \
    D(const D & t_) = default;                                  \
    D(D&&) = default;                                           \
    D & operator=(const D & rhs) = default;                     \
    D & operator=(D&&) = default;                               \
    operator T & () { return t; }                               \
    bool operator==(const D & rhs) const { return t == rhs.t; } \
    bool operator<(const D & rhs) const { return t < rhs.t; }   \
};

Live Demo

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJendasView Question on Stackoverflow
Solution 1 - C++Richard HodgesView Answer on Stackoverflow
Solution 2 - C++kamikazeView Answer on Stackoverflow
Solution 3 - C++JendasView Answer on Stackoverflow
Solution 4 - C++TartanLlamaView Answer on Stackoverflow