strong_type icon indicating copy to clipboard operation
strong_type copied to clipboard

value_of destroys strong type system to much

Open Viatorus opened this issue 3 years ago • 6 comments

I am evaluating which strong type library we could use in your environment.

I really like the simplistic of your implementation. But one thing botters me. Maybe you can help me, if I oversee a function or oversee the concrete concept here.

Is the ADL function value_of the only way to get the underlying value? If so, consider following example:

using my_strong_type = strong::type<int, struct foo>;

my_strong_type value{10};

int i = value_of(value);

Everything looks fine, but if someone later changes the underlying value to double, the narrowing is maybe only visible through compiler warnings.

If I could explicit (like a static_cast in other strong_type implementations) define the target type, the compiler wouldn't even compile this code.

using my_strong_type = strong::type<double, struct foo>;

my_strong_type value{10};

int i = value_of<int>(value); // Whoops!

Viatorus avatar Dec 09 '21 16:12 Viatorus

I see what you mean. I have taken the meaning of value_of to be "I hereby leave the realm of safety and go to the underlying types, warts and all."

I've thought some about this, though, but I just haven't been able to figure out how to do what you want, in a way that doesn't completely break perfectly reasonable code. If you have an implementation idea in mind, I'd love to learn about your thoughts.

rollbear avatar Dec 09 '21 22:12 rollbear

So one idea is to not use value_of at all (as a coding-style rule) and add strong::convertible_to<T> to every strong type. So the static_cast<int> fails if the strong type is no longer an int. Doesn't really work if some want multiple convertible_to options.

Another idea would be to allow value_of to take a type. For the member function this is not breaking, since we can have both functions, with an without a type. For the friend function, we have to pass the type via another object.

template<typename T>
struct identitiy {};

 template<typename U, typename = std::enable_if_t<std::is_same<T, U>::value>>
  STRONG_NODISCARD
  constexpr T& value_of() & noexcept { return val;}

template<typename U, typename = std::enable_if_t<std::is_same<T, U>::value>>
  STRONG_NODISCARD
  friend constexpr T& value_of(identity<U>, type& t) noexcept { return t.val;}

/// ...

using my_strong_type = strong::type<double, struct foo>;

my_strong_type value{10};

int i = value.value_of<int>();

int j = value_of(strong::identity<int>{}, value);

If the ADL version wouldn't look that ugly, I would send a PR right away. ;) How can we do better?

Viatorus avatar Dec 10 '21 10:12 Viatorus

It's not only ugly, it also breaks all code currently using the library, which (IMO) is not acceptable. I've been toying with returning a proxy object that does the conversion in its turn, which works fine for your example, but fails miserable for auto val = value_of(my_typed_obj). Given how gladly C++ makes unsafe implicit conversions when initializing variables, auto is (quite ironically) the only type safe way, so making that one fail, is also a no-go.

Another way around this is, of course, to not rely 100% on a library, but to lean on your compiler. If you compile with -Wconversion -Werror (gcc/clang), which you absolutely should if you care the least about type safety, you're already home and dry (I believe).

rollbear avatar Dec 12 '21 20:12 rollbear

I would have provide both overloads (type safe and not) so this wouldn't break, I think.

Maybe I just go with value_of(...) and/or static_cast.

Viatorus avatar Dec 13 '21 08:12 Viatorus

Hello, I think depending on what comes next in the code there are 3 solutions:

  • implicit cast to int (give up safety)
  • using auto to store the return of value_of (generic code)
  • adding a static_assert to check the underlying type (so you can come back and think about what needs to be changed)

kiwixz avatar Dec 21 '22 13:12 kiwixz