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

5

24

I'm trying to emulate in C++ a distinct type from the Nim programming language. The following example won't compile in Nim because the compiler catches the variables e and d having different types (Error: type mismatch: got (Euros, float)) despite both being a float at the binary level:

type
  Euros = distinct float

when isMainModule:
  var
    e = Euros(12.34)
    d = 23.3
  echo (e + d)

One way to do this in C++ would be to write a wrapper class for floats. But this doesn't work well with APIs which export the type because the size won't be the same as float. Or even if a class' size matches the storage length of a float, it will never match the size of a char type. That will work if you also implement all possible operators for operations like addition, substraction, etc, but requires much typing and duplication of code.

Older questions like Creating a new primitive type have as accepted answer to use boost's strong typedef. However the typedef seems to work only for function type signatures, the typedef won't prevent two float-inherited types to be added together and their type completely changed (well, because there is just the illusion of a new type):

#include <boost/serialization/strong_typedef.hpp>
#include <stdio.h>

BOOST_STRONG_TYPEDEF(float, money);

void test(money a, float b)
{
    int t = a + b;
    printf("value is %d", t);
}

int main()
{
    money a(5.5);
    int euros(5);
    // This is not caught!
    int dollars = a + euros;
    printf("dollars %d\n", dollars);
    // But the compiler catches this misuse.
    test(euros, a);
}

But that's nearly it, the test() call won't work because the signature doesn't match, but the language still allows other operations to mangle types at will.

That same answer mentions C++0x to bring strong typedefs, so I looked for this new support and found that Bjarne Stroustrup himself gave a C++11 style keynote in 2012. Around minute 21 he starts talking about these new strong typedefs. If you download just the slides, page 19 starts talking about SI Units and later page 22 and 23 mention how this would be done. However, I have been unable to make the examples work. Here's the patchwork I managed to concoct:

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
    explicit Value(double d) : val(d) {} // construct a Value from a double
};

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
constexpr Value<Second> operator "" _s(long double d)
// a f-p literal suffixed by ‘_s’
{
return Value<Second> (d);
}
constexpr Value<Meter> operator "" _m(long double d)
// a f-p literal suffixed by ‘_m’
{
return Value<Meter> (d);
}

int main(void)
{
    Speed sp1 = 100_m / 9.8_s;
    return 42;
}

I'm trying to compile this under MacOSX with the latest Xcode 5.1.1 with the command line:

$ g++ unit.cpp -std=c++11
unit.cpp:13:25: error: constexpr function's return type 'Value<Second>' is not a
      literal type
constexpr Value<Second> operator "" _s(long double d)
                        ^
unit.cpp:5:8: note: 'Value<Unit<0, 0, 1> >' is not literal because it is not an
      aggregate and has no constexpr constructors other than copy or move
      constructors
struct Value {
       ^
unit.cpp:18:24: error: constexpr function's return type 'Value<Meter>' is not a
      literal type
constexpr Value<Meter> operator "" _m(long double d)
                       ^
unit.cpp:5:8: note: 'Value<Unit<1, 0, 0> >' is not literal because it is not an
      aggregate and has no constexpr constructors other than copy or move
      constructors
struct Value {
       ^
unit.cpp:26:20: error: no matching literal operator for call to 'operator "" _m'
      with argument of type 'unsigned long long' or 'const char *', and no
      matching literal operator template
    Speed sp1 = 100_m / 9.8_s;
                   ^
unit.cpp:26:28: error: no matching literal operator for call to 'operator "" _s'
      with argument of type 'long double' or 'const char *', and no matching
      literal operator template
    Speed sp1 = 100_m / 9.8_s;
                           ^
4 errors generated.

Maybe the examples given in the slides and I'm missing some more code? Does somebody have a complete example of what Bjarne was trying to demonstrate?

Damalis answered 18/5, 2014 at 19:33 Comment(9)
For currency I wouldn't want to use floating point types anyway, but implement a fixed point type. Then you could make an abstract Money class (possibly templated and using the Curiously recurring template pattern) and let different currencies inherit from this abstract Money class. While I understand the use of money is an example, the pattern can be reused for other types as well.Dumb
Care to elaborate on "But this doesn't work well with APIs which export the type because the size won't be the same as float." ? I don't think there is a single circumstance in which struct TWrapper { T val; /* non-virtual methods */ } will be larger than T. Even if there was, this should only be a small inefficency, not an ABI incompatbility.Youlandayoulton
And why do you think it cannot be the size of a char? try: struct x{char x;}; int main(){std::cout<<sizeof(x)<<"\n";} and read the number 1.Lattermost
You are asking for warnings when you used c++11 extensions. And defining functions in main.Lattermost
@delnan I had the impression that the compiler with certain optimisation levels can change the size of a structure/class for padding/alignment. Is this not the case? Using a struct seems to be much more simple and doesn't require external libs… so, why does boost's typedef exist at all if structures are the solution?Damalis
@GrzegorzAdamHankiewicz Alignment is not affected by optimization level because that would mean ABI incompatibilities (couldn't link object files created with different opt levels). Padding is inserted if a structure contains members of different alignment, or explicitly requested (__attribute__((aligned(X)))), but not for ordinary structures with a single member. BOOST_STRONG_TYPEDEF in fact expands to a structure definition, it only exists to reduce repeating tedious and complicated code every time you want such a type.Youlandayoulton
I've fixed Bjarne's code so now it does compile coliru.stacked-crooked.com/a/02347c9e8322a53cGardener
@Gardener still won't compiler under macosx or gcc++-4.7 but if you post it as answer I'll mark it accepted answer.Damalis
@Grzechu I guess GCC 4.7 doesn't support return type deduction for functions. The code should compile if you replace auto in the definition of division operator with Value<Unit<m1 - m2, k1 - k2, s1 - s2>> coliru.stacked-crooked.com/a/14795a1e0dcce9f4Gardener
D
7

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;
}
Damalis answered 23/5, 2014 at 16:51 Comment(0)
N
18

There are no strong typedefs in C++11. There is support for units with <chrono> but that is a totally different thing. Nobody can agree on what behaviour strong typedefs should have, exactly, so there has never been a proposal for them that got anywhere, so not only are they in neither C++11 nor C++14, there is no realistic prospect at this time that they will get into any future Standard.

Netti answered 18/5, 2014 at 19:43 Comment(6)
A shame at some point. There were already several attempts made to bring them in.Surf
Strong typedefs are like concepts and ranges- everybody seems to want them but nobody can agree on what they should be.Netti
I remember that there was strong work going on related to a specific proposal which was going well but not finished. I should also point that there is a beginning of concepts being standardised very soon (at least the type constraint part). Basically, this answer might be a little be too pessimistic compared to what is actually happening right now.Angry
Yes it would be quite useful. We use them here to discriminate Id 's of various entity objects. The implementation we use is borrowed from the CUJ article in march 2003 'True typedefs'. A strong typedef in the c++ language should support that they are different incompatible types. Sometimes you need to go to the basetype, so it would help if you can static_cast them to their base types and or back to the strong types. The cast may fail if the base types cannot agree.Nostril
A reasonable workaround with classes: fluentcpp.com/2016/12/08/strong-types-for-strong-interfacesKlink
MSVC workaround: learn.microsoft.com/en-us/cpp/cpp/…Fluorosis
O
9

Not sure this is what you want, it is ugly, but it works :) You can wrap the type into a template class,

template <typename T, int N> // N is used for tagging
struct strong_typedef
{
    using strong_type = strong_typedef<T,N>; // typedef for the strong type
    using type = T; // the wrapped type
    T value; // the  wrapped value
    
    strong_typedef(T val): value(val){}; // constructor
    strong_typedef(){value={};}; // default, zero-initialization
    
    // operator overloading, basic example: 
    strong_type operator+(const strong_type& rhs) const
    {
        return value + rhs.value;
    }

    // display it
    friend ostream& operator<<(ostream & lhs, const strong_typedef& rhs)
    {
        lhs << rhs.value;
        return lhs;
    }
};

then use it as

// these are all different types
strong_typedef<double, 0> x = 1.1; 
strong_typedef<double, 1> y = 2.2;
strong_typedef<double, 2> z = 3.3;

std::cout << x + x << std::endl; // outputs 2.2, can add x and x
// cout << x + y << endl; // compile-time ERROR, different types

x, y and z are 3 different types now, because of the different N-s used in the template. You can access the type and value using the fields type and value, like x::value (will be double 1.1). Of course if you directly typedef the struct_typedef::type, you're back to square one, as you are losing the strong type. So basically your type should be strong_typedef and not strong_typedef::type.

Occultism answered 18/5, 2014 at 20:48 Comment(9)
When I try something like strong_typedef<double, 0> z = x + y; the compiler complaints about invalid operands to binary expression. So I guess I have to implement all the mathematical operators for each type too?Damalis
@Grzegorz x and y are different types , so you can't add them - isn't that exactly the point of a "strong typedef"? You even used this exact example in your post // This is not caught! int dollars = a + euros;Ulda
x and y must be of the same type with z (i.e. strong_typedef<double,0>), and yes, you should overload the math operators, as the struct itself doesn't know how to "add" etc.Occultism
For example, overloading the + operator: strong_typedef<T,N>& operator+(const strong_typedef<T,N>& rhs){value+=rhs.value; return *this;}Occultism
You can to this once (using templated operators) so you don't have to overload the operators for each type.Occultism
@MattMcNabb The difference with regards to Nimrod at least is that there the distinct type inherits all operations, so you can add or subtract two euros together. In C++ you need to redefine everything because it is a new type, not inherited from a primitive one. Templating seems to be the way to go.Damalis
is not that ugly. did anyone implemented all operators like this?Francinefrancis
@vsoftco, operator+ should not change *this. (cout << x+x << end; // will change x)Leodora
@Leodora Absolutely right, I've modified it. In fact, it's preferably to have it as a binary friend operator.Occultism
D
7

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;
}
Damalis answered 23/5, 2014 at 16:51 Comment(0)
S
5

C++ compilers generally expect the command line option -std=c++11 (or -std=c++0x for slightly older ones, respectively) to activate C++11-support.

not supporting C++11 style at all.

No, it perfectly does. GCC 4.7.2's support can be checked here. To activate some experimental features, pass -std=gnu++11.

And Clang 3.4 actually supports pretty much everything in C++11 and already much out of C++1y.

Surf answered 18/5, 2014 at 19:41 Comment(1)
Thanks, with that switch the errors are different though, so I still can't get to make that little example work. Maybe the example is incomplete to fit the slides?Damalis
I
1

One alternative that does not involve typedef and is not strongly enforced by the compiler but makes it very hard for the programmer to make mistakes it to encode the unit in question as a struct member.

For my arduino project I have types like

template <typename T>
struct millisecond {
    T millisecond;
    static constexpr const struct millisecond<T> zero = { 0 };
};

template <typename T>
struct microsecond {
    T microsecond;
    static constexpr const struct microsecond<T> zero = { 0 };
};

and use like

auto time_diff = millisecond<unsigned long>::zero;
time_diff.millisecond = nowMilliseconds() - s_lastPollTime.millisecond;

So with this strategy the compiler does not stop you from mixing units, but if you do then the error will always scream at you:

total_expenses.euros = expence1.euros + expence2.dollars;
Investment answered 31/1, 2019 at 23:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.