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 };
}
\$\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.

\$\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\$ Feb 18, 2022 at 14:50

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.