akim
akim

Reputation: 8759

Compiler-enforced semantic types

Say I have a class representing automata, whose states are numbered (using state_t = unsigned) and whose transitons are also numbered (using transition_t = unsigned). Of course at some point I end up messing some calls because transition_t and state_t are the same type, so the compiler does not enforce the (semantic) type safety. That's easy to workaround by using a small class templated by a tag (struct transition_tag {}; struct state_tag {};), so now transition_t and state_t are incompatible, good!

/// Lightweight state/transition handle (or index).
template <typename Tag>
struct index_t_impl
{
  using index_t = unsigned;

  constexpr index_t_impl(index_t i)
    : s{i}
  {}

  // Disallow index1_t i{index2_t{42}};
  template <typename T>
  index_t_impl(index_t_impl<T> t) = delete;

  bool operator==(index_t_impl t) const
  {
    return s == t.s;
  }

  // Disallow index1_t{42} ==  index2_t{42};
  template <typename T>
  bool operator==(index_t_impl<T> t) const = delete;

  /// Default ctor to please containers.
  index_t_impl() = default;
  constexpr operator index_t() const { return s; }
  /// Be compliant with Boost integer ranges.
  index_t_impl& operator++() { ++s; return *this; }
  /// Be compliant with Boost integer ranges.
  index_t_impl& operator--() { --s; return *this; }

private:
  index_t s;
};

Further, I have two structures which are very much alike:

And then again I have this issue that I use std::vector<transition_t> for two completely different purposes. Of course, I could again introduce a wrapper templated by a tag, but then things become messy again. Public inheritance is very tempting (Thou shalt not inherit from std::vector)!

But really, I'm tired of ad-hoc solutions each time I want to introduce new types that are exactly like the base type, but just incompatible. Are there any recommendations on this regard? The public inheritance is really attractive, but wouldn't it introduce code bloat with tons on extra instantiations? Maybe the public composition (struct predecessors_t { std::vector<transition_t> v; };) as recommended by Crashworks (https://stackoverflow.com/a/4353276/1353549) is a better option that scales better?

Is there anything in sight in the future of C++ to address this new?

Upvotes: 4

Views: 482

Answers (1)

U007D
U007D

Reputation: 6318

This issue of getting compiler-enforced semantic types can crop up in all sorts of situations, from your situation to co-ordinate systems with different origins (where the values are all the same type (eg. int), but semantically, the types must not be mixed, because they represent offsets from different origins (x,y,z=0,0,0)--this occurs frequently in mathematics, where, when graphing the quadrant with positive x and y, the origin is in the lower left, and computer science where it is very common to place the origin upper left) to spaceship navigation (more on this last below).

In 2012, Bjarne Stroustrup gave an interesting talk on what he called type-rich programming introducing compiler-enforced semantic type safety with C++11 using templates, user-defined literals, a claimed no run-time overhead implementation and even tales of lessons learned from the Mars Climate Observer snafu ($350M spacecraft + mission lost because of lack of enforced semantic type safety). You can see the part of the talk where he covers semantic types here: https://youtu.be/0iWb_qi2-uI?t=19m6s

I've written up a sample code excerpt based on Stroustrup's demo code, updated to current standards and with the required operator overloads implemented). Unlike Bjarne's example, this one actually compiles. ;)

Gist of this code can be found here: https://gist.github.com/u-007d/361221df5f8c7f3466f0f09dc96fb1ba

//Compiled with clang -std=c++14 -Weverything -Wno-c++98-compat main.cpp -o main

#include <iostream>
#include <string>

template<int M, int K, int S>  //Meters, Kilograms, Seconds (MKS)
struct Unit
{
    enum { m=M, kg=K, s=S };
};

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

//Basic Semantic Units for MKS domain
using Meter = Unit<1, 0, 0>;
using Kilogram = Unit<0, 1, 0>;
using Second = Unit<0, 0, 1>;
using Second2 = Unit<0, 0, 2>;

//Semantic Value Types for MKS domain
using Time = Value<Second>;
using Distance = Value<Meter>;
using Mass = Value<Kilogram>;
using Speed = Value<Unit<1, 0, -1>>; //Speed is meters/second
using Acceleration = Value<Unit<1, 0, -2>>; //Acceleration is meters/second^2

//Operator overloads to properly calculate units (incomplete; for demo purposes)
Speed operator/(const Distance& lhs, const Time& rhs)
{
    return Speed(lhs.val / rhs.val);
}

Acceleration operator/(const Speed& lhs, const Time& rhs)
{
    return Acceleration(lhs.val / rhs.val);
}

//Define literals
constexpr Distance operator"" _m(long double ld)
{
    return Distance(static_cast<double>(ld));
}

constexpr Mass operator"" _kg(long double ld)
{
    return Mass(static_cast<double>(ld));
}

constexpr Time operator"" _s(long double ld)
{
    return Time(static_cast<double>(ld));
}

constexpr Acceleration operator"" _s2(long double ld)
{
    return Acceleration(static_cast<double>(ld));
}

int main()
{
    Speed sp = Distance(100)/Time(9.58); //Not bad, but units could be more convenient...
    Distance d1 = 100.0_m; //A good distance to run a race
    Speed sp1 = 100.0_m/9.58_s; //A human can run this fast
//    Speed sp2 = 100.0_m/9.8_s2; //Error: speed is m/s, not m/s^2
//    Speed sp3 = 100.0/9.8_s; //Error: 100 has no unit
    Acceleration ac1 = sp1/0.5_s; //Faster than any human

    return EXIT_SUCCESS;
}

Upvotes: 9

Related Questions