Strong Typedefs

Strong typedefs

Quoting cplusplus.com,

Note that neither typedef nor using create new distinct data types.
They only create synonyms of existing types.
That means that the type
of myword above, declared with type WORD, can as well be considered of
type unsigned int; it does not really matter, since both are actually
referring to the same type.

Since int and handle are one and the same, the output 0 1 is expected.

There's a workaround though, as @interjay suggests.

You can use BOOST_STRONG_TYPEDEF.

BOOST_STRONG_TYPEDEF( int , handle );

Strong typedef for primitive types (BOOST_STRONG_TYPEDEF is not cutting it)

  • Reading http://www.boost.org/doc/libs/1_63_0/libs/serialization/doc/strong_typedef.html
    , it seems that your example using boost compiles because the types created by boost are substitutable to the original type which are comparable.

  • Johnathan Boccara from fluentcpp provides an implementation of strong types on its github that should be what you want:

#include <cassert>
#include "NamedType/named_type.hpp"

int main() {
using TIMER_ID =
fluent::NamedType<unsigned int, struct TimerIdTag, fluent::Comparable>;
using PROCESS_ID =
fluent::NamedType<unsigned int, struct ProcessIdTag, fluent::Comparable>;

TIMER_ID a(123);
PROCESS_ID b(456);

assert(a == a);
// assert(a == b); doesn't compile
return 0;
}

Strongly typed using and typedef

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

strong typedef of std::string

You won't need to wrap every operation of std::string because you won't need to use them. All that the caller has to do is initialise the argument with a correct type with a value for the internal string:

struct address_type {
std::string value;
};

struct name_type {
std::string value;
};

struct info {
info(address_type address, name_type name)
: address_{std::move(address.value)}, name_{std::move(name.value)}
{}
info(address_type address)
: address_{std::move(address.value)}
{}
info(name_type name)
: name_{std::move(name.value)}
{}
private:
std::string address_;
std::string name_;
};

info with_address {address_type{"str"}};
info with_name {name_type {"str"}};

But if you want to proceed with your approach of providing string functionality with your custom type, then you must wrap all the operations. That's going to be a ton of boilerplate and there's no magic to avoid that.

How can I create a new primitive type using C++11 style strong typedefs?

There are several ways to solve this, but since I was looking for a fix to Bjarne's code in the presentation slides, I'm accepting this answer which @robson3.14 left in comments to the question:

#include <iostream>

template<int M, int K, int S> struct Unit { // a unit in the MKS system
enum { m=M, kg=K, s=S };
};

template<typename Unit> // a magnitude with a unit
struct Value {
double val; // the magnitude
// construct a Value from a double
constexpr explicit Value(double d) : val(d) {}
};

using Meter = Unit<1,0,0>; // unit: meter
using Second = Unit<0,0,1>; // unit: sec
using Speed = Value<Unit<1,0,-1>>; // meters/second type

// a f-p literal suffixed by ‘_s’
constexpr Value<Second> operator "" _s(long double d)
{
return Value<Second> (d);
}
// a f-p literal suffixed by ‘_m’
constexpr Value<Meter> operator "" _m(long double d)
{
return Value<Meter> (d);
}
// an integral literal suffixed by ‘_m’
constexpr Value<Meter> operator "" _m(unsigned long long d)
{
return Value<Meter> (d);
}

template<int m1, int k1, int s1, int m2, int k2, int s2>
Value<Unit<m1 - m2, k1 - k2, s1 - s2>> operator / (Value<Unit<m1, k1, s1>> a, Value<Unit<m2, k2, s2>> b)
{
return Value<Unit<m1 - m2, k1 - k2, s1 - s2>>(a.val / b.val);
}

int main()
{
Speed sp1 = 100_m / 9.8_s;
std::cout << sp1.val;
}

Using Boost strong typedef with unordered_map

From the boost documentation about BOOST_STRONG_TYPEDEF, it is clearly mentioned that it defines a new type wrapping the target inner type (it is not a simple alias).

Knowing that, there is no defined std::hash<MyInt> specialization available (required by std::unordered_map<MyInt, int>) since MyInt is now a distinct type from uint64_t.

You just need to provide one and it should work as expected.

For instance:

namespace std
{
template <>
struct hash<MyInt>
{
size_t operator()(const MyInt & m) const
{
return hash<uint64_t>{}(m);
}
};
}

Edit:

As pointed out in comments, you could also directly pass the proper std::hash<> specialization to use from the std::unordered_map<> instantiation as the third template parameter since MyInt is implicitly convertible to uint64_t (as mentioned in the boost documentation as well).

It would then become:

std::unordered_map<MyInt, int, std::hash<uint64_t>> umap;

No need to define your own std::hash<MyInt> specialization anymore.

BOOST_STRONG_TYPEDEF and move semantics

The fundamental reason would be that many boost libraries are still targetting pre-c++11 standard compilers¹, i.e. compilers that do not support move semantics.

I suppose for some compilers the tricks in Boost Move might be used, but I'm not sure that would be easy to apply transparently to strong typedefs.

However, I'd always roll my own strong typedef to do the job, as I've suggested fro other reasons, e.g. How do I strongly typedef non-primitive types?


¹ Boost Serialization is definitely one of these libraries

Why C++20 format has no strong typedef for format string?

The point of strong typedefs is to prevent this from working:

void takes_id(SomeIdType);
takes_id(42);

The point of format is to allow this to work:

format("User {} owes me {} points.", name, 100);

That is a string literal. Requiring a strong type means more burden on the users, having to write something like this:

format(format_string("User {} owes me {} points."), name, 100);

This isn't a burden on the typical strong typedef use case, since you will actually be trafficking in SomeIdTypes. You'll have a function that gives you a SomeIdType, you'll store a member of type SomeIdType. Basically, the amount of actual conversions will be fairly minimal... so on the call site you would just write takes_id(my_id) and the code mostly just looks the same, with added safety.

But the overwhelmingly common case for formatting is to use string literals, so that's a lot of added annotation.

The nominal benefit of strong typing is to catch users doing maybe something like this:

format(name, "User {} owes me {} points.", 100);

Or even:

format(name, 100);

The former seems unlikely to ever happen. The latter is certainly possible, if the first argument happens to be sufficiently string-like. But is this a sufficiently common problem as to force everyone to write more code? I don't think so.

Now, if string literals had their own distinct type from const char[N] (and I really wish they did), then it would be possible to create a type that is implicitly constructible from std::string_literal but needs to be explicitly constructed from std::string_view. And if that were a thing, then the API probably would've used that - since this would require no annotation in the common case and not using string literals seems sufficiently rare that requiring an explicit cast seems... fine?

Besides, on the question of safety, the issue isn't so much passing the wrong kind of string as actually being able to verify the value of it in its context:

format("User {} owes me {} points.", name);

We'd really like for this not to compile, even though we provided a format string in the correct spot. And it appears to be possible to do this. But we don't need strong typedefs for this either, we just need the ability to know if the format string is a constant expression or not.


To summarize, the answer to:

but I wonder why is there not an overload that accepts a "strong typedef"

is that this requires users to provide more call-side annotation while providing very minimal benefit. It would only catch wrong uses in the rarest of uses, so seems like a fairly bad trade-off.

Using strong typedef as a more lightweight alternative to Boost Parameter library?

Technically speaking:

  • it works
  • it adds type safety

Practically speaking:

I would not recommend creating new types just for the sake of a single function's parameters (unless it is an enum specific to this function), types should permeate the application to avoid casts being used over and over.

If the types X, Y, Width and Height are used throughout the application, then not only will there be no cast, but your application will be much safer and much better documented too (yeah... I am a type freak).

Now, with regard to Boost.Parameters, this is completely different.

Boost.Parameters can (potentially) be added when you have types already in place. Honestly though I never saw the need. When your functions grow so unwieldy that Boost.Parameters is required to call them, you should fix the functions, not add to the clutter.



Related Topics



Leave a reply



Submit