How can I create a new primitive type using C++11

2019-01-23 13:47发布

问题:

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?

回答1:

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.



回答2:

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.



回答3:

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)
    {
        value+=rhs.value; 
        return *this;
    }

    // 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.



回答4:

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