3
\$\begingroup\$

Similar to C++ int_cast<> function for checked casts?, but C++20, and with target type deduced from context.

The goal is to implement a runtime check that the value being converted can be represented in the target type, and throw an exception if it can't.

This basically works for my use case, but I wonder if there's anything obvious I've missed.

Use as

std::uint32_t x = 0x40000000;
std::int32_t y = checked_cast(x); // works
std::uint16_t z = checked_cast(x); // throws

Code:

#pragma once
#include <stdexcept>
#include <utility>
class bad_checked_cast : public std::logic_error
{
public:
 bad_checked_cast() : std::logic_error("Bad checked_cast") { }
};
template<typename From>
struct checked_cast_result_proxy
{
 template<typename To>
 operator To() const
 {
 if(std::in_range<To>(value))
 return static_cast<To>(value);
 else
 throw bad_checked_cast();
 }
 From value;
};
template<typename From>
auto checked_cast(From value) -> checked_cast_result_proxy<From>
{
 return checked_cast_result_proxy<From> { value };
}
Toby Speight
87.1k14 gold badges104 silver badges322 bronze badges
asked Feb 17, 2022 at 22:00
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

There's an argument (no pun intended!) that it's not an error in the program logic for the value to be out of the convertible range. I would suggest using std::domain_error as the base of bad_checked_cast.


We should constrain the template to reject types that std::in_range() doesn't accept:

#include <concepts>
⋮
template<typename From>
 requires std::integral<From>
struct checked_cast_result_proxy
⋮
 template<typename To>
 requires std::integral<To>
 operator To() const

std::in_range cannot be used with

enums (including std::byte), char, char8_t, char16_t, char32_t, wchar_t and bool.

That's harder to enforce, as there isn't a concept that expresses that restriction (and the library I'm using allows invocation, but than fails at a static_assert).


I found it useful to have a plain function interface to collapse the proxy object, for when there isn't a specific enough context to do so (e.g. passing into another template function that requires an arithmetic type):

template<typename To, typename From>
 requires std::integral<From> && std::integral<To>
static To checked_cast_to(From from) { return checked_cast(from); }

That enabled me to test throwing with this:

TEST(CheckedCast, success)
{
 const std::uint32_t x = 0x40000000;
 std::int32_t y = checked_cast(x);
 EXPECT_EQ(x, y);
}
TEST(CheckedCast, fail)
{
 const std::uint32_t x = 0x40000000;
 EXPECT_THROW(checked_cast_to<std::uint16_t>(x), std::exception);
}

You could consider generalising to make it work with other (non-integer) arithmetic types (e.g. floating and complex numbers). That might be a lot of work that's not useful for you, though.

answered Feb 18, 2022 at 8:34
\$\endgroup\$
1
  • \$\begingroup\$ Allowing the caller to manually specify the target-type is a good idea. But using a different name for that is so inconvenient. \$\endgroup\$ Commented Feb 18, 2022 at 14:50

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.