joboccara / NamedType

Implementation of strong types in C++
MIT License
769 stars 84 forks source link

Explicit underlying cast #6

Open Guillaume227 opened 6 years ago

Guillaume227 commented 6 years ago

In my project I need to:

I thought that need is general enough that it would be useful to all NamedType users and that it's not taking away any existing feature, hence that PR.

joboccara commented 6 years ago

Guillaume,

Thanks a lot for contributing to NamedType. I am sorry for the late reply, I didn't see the PR earlier. In the meantime, NamedType has had the FunctionCallable skill (see https://github.com/joboccara/NamedType/blob/master/underlying_functionalities.hpp#L84-L94), which is an opt-in for allowing implicit conversion to underlying type. Do you think this fills your need to casting to the underlying value? Also, would you have an example of use case for the value initialization please?

Guillaume227 commented 6 years ago

Hi Jonathan, thanks for getting back to me! I have moved on from my previous job where I introduced your lib but from what I can remember (I don't have access to the source code anymore) here is the use case behind that PR. I had a named type NT wrapping an int. Then I had a template that I wanted to call on int-convertible types, like NT or float, or an enum class so I was using static_cast. I didn't want to add a specialization just for the purpose of calling .get() instead of doing a straight static_cast. I thought the explicit nature of the constructor played nicely with the strong type nature. So basically it boils down to a 'static_castable' skill. I don't think FunctionCallable would help in my case.

joboccara commented 6 years ago

Guillaume, would the ImplicitlyConvertibleTo skill help more in your case then? Here is the rationale and usage for that skill.

Guillaume227 commented 6 years ago

ImplicitlyConvertibleTo would defeat my purpose for using NamedType as I want a cast to be explicit. Just curious: what do you think would the downsides be of adding that explicit conversion operator? Not a big deal if we just ignore and close that PR.

joboccara commented 6 years ago

No major downside, it only gives more access to the underlying type, and the STL doesn't do it with unique_ptr. But I understand the need. Would it make sense for you if it was implemented as a skill "ExplicitlyConvertible"?

Guillaume227 commented 6 years ago

Yes, it would make good sense to have it as a skill! I need to find sometime to rework it then.

arvidn commented 6 years ago

+1 on having an ExplicitlyConvertible skill

arvidn commented 6 years ago

In fact, I would argue that making all NamedType explicitly convertible to its underlying type makes more sense than to expose the get() functions. static_cast<> is the standard protocol for getting at the underlying type, and it would make sense for interoperability to support this protocol.

One missing piece is a protocol for querying the underlying type, something like std::underlying_type. Unfortunately it seems the standard type trait doesn't allow specializations for custom types, and is really only meant for enums.

I suppose generic code could be made to support the direct member UnderlyingType, possibly by using the detection idiom, it would be nicer to have a type trait for this.

joboccara commented 6 years ago

Arvid, thanks for your suggestion. Are you thinking about particular examples that use static_cast<> as a standard protocol for getting the underlying value?

arvidn commented 6 years ago

I think the most obvious example is for enums. static_cast<> is what you use to get the underlying integer value. I would argue that the fact that the language provides explicit conversion operators is an indication of making that the standard mechanism for conversions (and for explicit conversions, you use static_cast<> to invoke the conversion).

I would imagine that generic code use static_cast<> for this reason.

The other major example I have in mind is boost.endian. I'm using this to build network packets, to abstract alignment and endian requirements. Currently, boost.endian doesn't really have support for strong types, but only supports raw integers. However, I would greatly improve its type safety if the value_type used in boost.endian could be something more restricted than a raw integer. But since boost.endian is responsible for the actual binary representation of the value, it needs to deal with raw integer types, that it can shift, AND and OR. In a way, it deals with the storage of values, and the mapping of the storage to values. In order for boost.endian to support arbitrary value types, it would need to have a protocol to:

  1. cast a value to its underlying integer
  2. query the underlying integral type (via some type trait)

I'm interested in proposing a patch to boost.endian to support this.

viboes commented 6 years ago

An alternative is to build on top of Boost.Endian an endian type that can take a UDT types instead of an integral builtin type.

viboes commented 6 years ago

Yes, the standard conversion protocol uses static_cast.

I believe both skills (implicit and explicit conversion to) are useful and must not be hard coded in the strong type base type (NamedType in this case).

However, in order to get the underlying type I believe that conversion are cumbersome. We need something else. A underlying(st) non-memberfunction that returns a reference to theunderlying_type`. This should work for enums as for strong types that wrap an underlying type.

BTW, I believe that get is a function name that has no clear intent, IMHO is is misused in the standard. I believe we need something more specific for strong types as st.get_underlying() or st.underlying().

arvidn commented 6 years ago

@viboes Yes, once I actually started working on it, with the intention to patch boost.endian, it turned out to be much simpler to build a new primitive on top of boost.endian that glues together both NamedType, enums and some other types that I had (that already supported a tag template parameter for unique types)

viboes commented 6 years ago

Maybe it is worth understanding why std::duration hasn't an explicit cast to his representation (With H.Hinnant). Instead it has a count() member function. If we wanted to be able to implement std::duration using the library+patch, the user would need to delete the explicit conversion operator, which is weird.