Reputation: 50190
I was reading this https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#S-philosophy and in P1 there is this
change_speed(double s); // bad: what does s signify?
// ...
change_speed(2.3);
A better approach is to be explicit about the meaning of the double (new speed or delta on old speed?) and the unit used:
change_speed(Speed s); // better: the meaning of s is specified
// ...
change_speed(2.3); // error: no unit
change_speed(23m / 10s); // meters per second
And this made me wonder (since I like the idea a lot) how one would implement the type 'Speed'.
I tried this
using Speed = double;
void change_speed(Speed s)
{
cout << s;
}
but this is not strong enough, since I can still do
double j = 43.0;
change_speed(j);
and
change_speed(42.0);
Note: If Speed type can be made to work as I would like then the last one would still be needed, but would have to be a cast:-
change_speed(static_cast<Speed>(42.0));
So then I tried to create a one field struct (later to be templatized) for it. But realized that I needed every single operator overload (and maybe some operators that dont even exist) And that seemed waaaay too much.
So , is there a way to do this?
Upvotes: 1
Views: 782
Reputation: 334
You do have to make a new class with the conversion behavior and operators that you want. However there are some libraries that can help simplify the process. For example, type safe
For integral types, you can also use an empty enum in C++17. For example,
enum class Strong : int {};
void func(Strong);
int main() {
// func(15); doesnt compile
func(Strong{15});
func(static_cast<Strong>(15));
auto s = Strong{15};
int x = static_cast<int>(s);
}
Edit:
To elaborate, different capabilities can be designed in a strong type so the solution ultimately depends on the exact behavior you want. If you are looking to provide shortcuts or building blocks for creating strong types, type_safe is the best thing ive seen for that and maybe you could look at the implementation for ideas.
I'll share my thoughts on an example case where we want to create a type Strong
that resembles an int
.
A key choice is construction, or conversion from the underlying type. basically the choice is between an explicit or implicit constructor i.e. explicit Strong(int)
or Strong(int)
. if you want something like change_speed(42.0);
to build then go with implicit, if you want to require change_speed(static_cast<Speed>(42.0));
go explicit.
You could then further specify how you want to convert back to the underlying type. maybe an explicit conversion operator if you want to require static cast to be used (e.g. explicit operator int()
), or an implicit conversion operator if you are okay with implicit conversions (e.g. operator int()
). maybe you dont want to use a conversion operator at all, and you'd rather provide a free function for accessing the underlying type (e.g. int underlying(Strong const&)
). If you want to be able to write auto x = Strong{}; int y = x;
then you need an implicit conversion operator. If not, use an explicit operator or a free function.
For operators you can of course choose which ones to add to the strong type, and for the binary operators you can choose to provide asymmetric operators or not (e.g. can Strong
and int
be added together?). although of course this choice interacts with your previous choices. if youve allowed for implicit conversion from the underlying type then a symmetric operator will already support asymmetric operations. for example Strong operator+(Strong, Strong)
will compile if one argument is int
as long as you have an implicit constructor for int
in your definition of Strong
.
In short, it really does depend on what you want. But hopefully this gives you a better idea of some of the choices available and their resulting effects.
There is even more to discuss if we're talking about a full on unit system (meters, kilograms etc.). For that you could look at Boost.Units for ideas.
Upvotes: 4
Reputation: 597051
how one would implement the type 'Speed'.
You would have to implement it as a struct
or class
, and also define user-defined literals to construct Speed
objects from expressions like 23_m / 10_s
, eg:
struct Speed
{
double value;
explicit Speed(double value) : value(value) {}
...
};
struct Meters
{
unsigned long long value;
explicit Meters(unsigned long long value) : value(value) {}
...
};
struct Seconds
{
unsigned long long value;
explicit Seconds(unsigned long long value) : value(value) {}
...
};
Meters operator "" _mtrs(unsigned long long value) {
return Meters{value};
}
Seconds operator "" _secs(unsigned long long value) {
return Seconds{value};
}
Speed operator/(const Meters &m, const Seconds &s)
{
return Speed{static_cast<double>(m.value) / s.value};
}
...
void change_speed(const Speed &s)
{
...
}
change_speed(23_mtrs / 10_secs);
Upvotes: 1
Reputation: 238401
how one would implement the type 'Speed'.
The type would have to be a class.
The most important detail is an explicit converting constructor from double
.
So then I tried to create a one field struct (later to be templatized) for it.
Ah. Good start.
But realized that I needed every single operator overload ... And that seemed waaaay too much.
I suppose then the next step is to get over the feeling of being overwhelmed and start programming.
Or, you could instead make the class convertible back to double, and only use the extracted value for calculations. This would limit the usefulness of the class to function arguments.
Upvotes: 0