Even though static_cast is assumed to be the safest of all C++ casts, it can still be the cause of hard-to-find bugs, or unforeseen crashes. These bugs most often occur when casting between signed and unsigned types, or casting values into types which are not large enough to hold them.
The following code shows some examples which will fail when using static_cast, being unnoticeable most of the time:
// oops, unsigned type cannot hold negative number char c = -1; unsigned char uc = static_cast<unsigned char>(c); // oops, 65536 is too big for an unsigned short unsigned int i = 65536; unsigned short us = static_cast<unsigned short>(i); // oops, 65000 is too big for the signed type unsigned short us = 65000; short s = static_cast<short>(us);
Sometimes, there is no way around casting because some APIs require certain types, like in the following exemplary std::vector<>::size() implementation:
size_t vector<T>::size(void) const { return (static_cast<size_t>(m_end - m_start)); }
As you can guess, m_end and m_start are both pointers, so the expression m_end – m_start will yield a ptrdiff_t, and not size_t. But the STL API dictates the return type of std::vector<>::size() to be of type size_t, hence it has to be casted.
Another application of signed/unsigned casting sometimes appears in templated code, where you don’t know the underlying type you’re dealing with, but e.g. need to cast it into its unsigned representation because it is used in bit-shifting operations (shifting of signed integers can invoke undefined or implementation-defined behaviour), like in the following example:
template <typename T> void Do(T value) { // do something with value... // cast value into its unsigned type, used for shifting typedef typename std::make_unsigned<T>::type UnsignedT; UnsignedT mask = static_cast<UnsignedT>(value) << bit_mask; }
Therefore, it would be nice to have a safer variant of static_cast which actually checks whether the type casted into can hold the given value.
The requirements for such a safe_static_cast operator are the following:
- The syntax should exactly match the common static_cast syntax.
- A safe_static_cast should assert whenever a value cannot be held by the type casted into.
- In retail builds, a safe_static_cast should behave like an ordinary static_cast.
Keeping those requirements in mind, let’s start by defining such an operator (or rather, function template):
template <typename TO, typename FROM> inline TO safe_static_cast(FROM from) { #if ME_MASTER return static_cast<TO>(from); #else return safe_static_cast_impl<TO>(from); #endif }
Because the compiler can automatically deduce the template argument type FROM from the given value, only type TO needs to be specified explicitly when using the function template, leading to the same syntax as static_cast:
char c = -1; // type FROM (char) is automatically deduced from the argument (c) unsigned char uc = safe_static_cast<unsigned char>(c);
The only thing left missing for now is the actual implementation of safe_static_cast_impl, which is not too hard to come up with:
template <typename TO, typename FROM> inline TO safe_static_cast_impl(FROM from) { ME_ASSERT(from >= std::numeric_limits<TO>::min(), "Number to cast exceeds numeric limits.")(from, TO(), std::numeric_limits<TO>::min(), std::numeric_limits<TO>::max()); ME_ASSERT(from <= std::numeric_limits<TO>::max(), "Number to cast exceeds numeric limits.")(from, TO(), std::numeric_limits<TO>::min(), std::numeric_limits<TO>::max()); return static_cast<TO>(from); }
Unfortunately, this has the slight annoyance that depending on FROM and TO, the compiler will now warn about signed/unsigned mismatches in our comparisons inside the assert, and rightly so. What we therefore need is a mechanism for choosing different implementations based on the signedness of both types.
Unsurprisingly, class templates and template specializations nicely fit the bill:
// base template template <bool IsFromSigned, bool IsToSigned> struct safe_static_cast_helper; // template specialization for casting from an unsigned type into an unsigned type template <> struct safe_static_cast_helper<false, false> { template <typename TO, typename FROM> static inline TO cast(FROM from) { ME_ASSERT(from >= std::numeric_limits<TO>::min(), "Number to cast exceeds numeric limits.")(from, TO(), std::numeric_limits<TO>::min(), std::numeric_limits<TO>::max()); ME_ASSERT(from <= std::numeric_limits<TO>::max(), "Number to cast exceeds numeric limits.")(from, TO(), std::numeric_limits<TO>::min(), std::numeric_limits<TO>::max()); return static_cast<TO>(from); } }; // template specialization for casting from an unsigned type into a signed type template <> struct safe_static_cast_helper<false, true> { template <typename TO, typename FROM> static inline TO cast(FROM from) { ME_ASSERT(from >= std::numeric_limits<TO>::min(), "Number to cast exceeds numeric limits.")(from, TO(), std::numeric_limits<TO>::min(), std::numeric_limits<TO>::max()); ME_ASSERT(from <= std::numeric_limits<TO>::max(), "Number to cast exceeds numeric limits.")(from, TO(), std::numeric_limits<TO>::min(), std::numeric_limits<TO>::max()); return static_cast<TO>(from); } }; // template specialization for casting from a signed type into an unsigned type template <> struct safe_static_cast_helper<true, false> { template <typename TO, typename FROM> static inline TO cast(FROM from) { // make sure the input is not negative ME_ASSERT(from >= 0, "Number to cast exceeds numeric limits.")(from, TO(), std::numeric_limits<TO>::min(), std::numeric_limits<TO>::max()); // assuring a positive input, we can safely cast it into its unsigned type and check the numeric limits typedef typename std::make_unsigned<FROM>::type UnsignedFrom; ME_ASSERT(static_cast<UnsignedFrom>(from) >= std::numeric_limits<TO>::min(), "Number to cast exceeds numeric limits.")(from, TO(), std::numeric_limits<TO>::min(), std::numeric_limits<TO>::max()); ME_ASSERT(static_cast<UnsignedFrom>(from) <= std::numeric_limits<TO>::max(), "Number to cast exceeds numeric limits.")(from, TO(), std::numeric_limits<TO>::min(), std::numeric_limits<TO>::max()); return static_cast<TO>(from); } }; // template specialization for casting from a signed type into a signed type template <> struct safe_static_cast_helper<true, true> { template <typename TO, typename FROM> static inline TO cast(FROM from) { ME_ASSERT(from >= std::numeric_limits<TO>::min(), "Number to cast exceeds numeric limits.")(from, TO(), std::numeric_limits<TO>::min(), std::numeric_limits<TO>::max()); ME_ASSERT(from <= std::numeric_limits<TO>::max(), "Number to cast exceeds numeric limits.")(from, TO(), std::numeric_limits<TO>::min(), std::numeric_limits<TO>::max()); return static_cast<TO>(from); } };
As you can see, the base template struct safe_static_cast_helper is specialized for all 4 possibilities that stem from casting:
- Casting from an unsigned type into an unsigned type.
- Casting from an unsigned type into a signed type.
- Casting from a signed type into an unsigned type.
- Casting from a signed type into a signed type.
Inside those specializations, we can make sure that the type TO can hold the value provided by FROM, which is easier when knowing the signedness of each type. For example, when casting from signed types into unsigned types, we can assert that the value to be casted is not negative.
The other parts of the specializations’ implementation should be self-explanatory, but there is still something missing: how do we call the “correct” specialization from inside the safe_static_cast function template?
Using std::numeric_limits<>, the solution is easy:
template <typename TO, typename FROM> inline TO safe_static_cast(FROM from) { #if ME_MASTER return static_cast<TO>(from); #else // delegate the call to the proper helper class, depending on the signedness of both types return safe_static_cast_helper<std::numeric_limits<FROM>::is_signed, std::numeric_limits<TO>::is_signed>::cast<TO>(from); #endif }
The traits class std::numeric_limits<>::is_signed will yield either false or true, based on the signedness of the given type, which in turn is used to choose the respective safe_static_cast_helper template specialization.
Finally, let’s take a look at what happens when using safe_static_cast instead of static_cast in the examples shown at the beginning of the post:
char c = -1; unsigned char uc = safe_static_cast<unsigned char>(c);
Running the above will yield the following output:
(ASSERT) Assertion "from >= 0" failed. Number to cast exceeds numeric limits. (ASSERT) o Variable from = -1 (char) (ASSERT) o Variable TO() = 0 (unsigned char) (ASSERT) o Variable std::numeric_limits<TO>::min() = 0 (unsigned char) (ASSERT) o Variable std::numeric_limits<TO>::max() = 255 (unsigned char)
Similarly for other cases. Running the following:
unsigned int i = 65536; unsigned short us = static_cast<unsigned short>(i);
yields
(ASSERT) Assertion "from <= std::numeric_limits<TO>::max()" failed. Number to cast exceeds numeric limits. (ASSERT) o Variable from = 65536 (unsigned int) (ASSERT) o Variable TO() = 0 (unsigned short) (ASSERT) o Variable std::numeric_limits<TO>::min() = 0 (unsigned short) (ASSERT) o Variable std::numeric_limits<TO>::max() = 65535 (unsigned short)
And that’s all there is to it.
AFAICS “safe_static_cast” works for fundamental types only, doesn’t it?
Not quite. It works for integral types, as well as types which can be promoted to such types (e.g. enums), and classes which can be converted into such types (e.g. classes offering respective cast operators).
If I am not mistaken in the specialization of safe_static_cast_helper for signed to unsigned types you are casting ‘from’ to unsigned int (unsigned being a synonym for unsigned int). Wouldn’t it be better to cast ‘from’ to std::make_unsigned_t?
You are absolutely right.
Using std::make_unsigned would be much better, in the latest revision of my implementation I use my own traits class that basically does the same thing (didn’t assume C++11 at the time of writing the implementation).
The post has been updated accordingly.