58

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.

Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
Jendas
  • 3,359
  • 3
  • 27
  • 55
  • 5
    Just add explicit before c-tor. – firescreamer Dec 15 '15 at 11:27
  • If you do that for strings, using the type can easily lead to undefined behavior: `SAFE_TYPEDEF(std::string, S); std::string* s = new S(); delete s;` `std::string` is not meant to be used as a base-class. – Jens Dec 15 '15 at 11:34
  • `portal_id = cake_id` works, because with this construct `CakeID` can be passed wherever a string is expected - e.g to the assignment operator of `PortalId`. Inheritance defines an "is a"-relationship. – cdonat Dec 15 '15 at 11:39
  • The issue with defining a new type with a terse syntax that implicitly defines operations is that not all operations are relevant to these distinct types, depending on intent that can only be described in detail by the programmer. – curiousguy Jan 08 '19 at 14:26

4 Answers4

26

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;
}
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
17

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>;
kamikaze
  • 1,529
  • 8
  • 18
  • 1
    The problem with this approach is that the safe_str's of different types are both publicly derived from string. They are therefore convertible to each other. This means that the safe_str is not actually safe at all. – Richard Hodges Dec 15 '15 at 16:39
  • 3
    @RichardHodges you can convert them, but I don't how you would do it by accident. – kamikaze Dec 16 '15 at 07:34
  • 2
    you're right. string's constructor is explicit. Nice. :) – Richard Hodges Dec 16 '15 at 09:43
  • @RichardHodges Which constructor is explicit? – curiousguy Jan 07 '19 at 22:27
  • @curiousguy fewer of them than I originally thought. – Richard Hodges Jan 08 '19 at 06:54
  • 1
    @RichardHodges In traditional C++, it only makes sense to declare one argument ctors (that aren't copy ctors) as explicit, because *there are no other case where a constructor declaration creates an implicit conversion*. (In modern C++ there is brace init so it's slightly different.) – curiousguy Jan 08 '19 at 14:10
  • maybe I miss something, but doesnt public inheritance mean that you can pass either of the two to a function taking a `std::string&` ? – 463035818_is_not_an_ai Jan 30 '19 at 11:28
  • This solution does not prevent `portal_id = "1.0"; ` from compiling neatly. – P.W Jan 30 '19 at 11:54
8

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.

Jendas
  • 3,359
  • 3
  • 27
  • 55
5

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

TartanLlama
  • 63,752
  • 13
  • 157
  • 193
  • Ah, named types, that was the term I was unable to recollect. – Jendas Dec 15 '15 at 11:25
  • Also I am quite surprised that named types were not already introduced in c++14. – Jendas Dec 15 '15 at 11:30
  • Guessing the downvotes were because I didn't provide a full solution? There's one there now. – TartanLlama Dec 15 '15 at 11:36
  • Is this solution lifted from boost? If so I think it needs looking at since it's breaking the rule of 5 by not providing a move constructors and assignment. – Richard Hodges Dec 15 '15 at 11:49
  • @RichardHodges Yes, this is lifted from Boost with minor changes, as I said. Looks like most of it would be fine with `default`ed special members. – TartanLlama Dec 15 '15 at 11:54