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 };
}
1 Answer 1
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 andbool
.
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.
-
\$\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\$Deduplicator– Deduplicator2022年02月18日 14:50:37 +00:00Commented Feb 18, 2022 at 14:50