DRTVWR-575: Introduce llssize (signed size_t) and narrow() function.

llssize is for a function parameter that should accept a size or index
(derived from size_t, which is 64 bits in a 64-bit viewer) but might need to
go negative for flag values. We've historically used S32 for that purpose, but
Xcode 14.1 complains about trying to pass size_t to S32.

narrow() is a template function that casts a wider type (e.g. size_t or
llssize) to a narrower type (e.g. S32 or U32), with validation in
RelWithDebInfo builds. It verifies (using assert()) that the value being
truncated can in fact fit into the target type.
meow-7.2.2
Nat Goodspeed 2022-11-04 17:21:30 -04:00
parent a2a723f383
commit cd997f21c2
1 changed files with 102 additions and 4 deletions

View File

@ -26,16 +26,23 @@
#ifndef LL_STDTYPES_H
#define LL_STDTYPES_H
#include <cassert>
#include <cfloat>
#include <climits>
#include <limits>
#include <type_traits>
typedef signed char S8;
typedef signed char S8;
typedef unsigned char U8;
typedef signed short S16;
typedef unsigned short U16;
typedef signed int S32;
typedef signed int S32;
typedef unsigned int U32;
// to express an index that might go negative
// (ssize_t is provided by SOME compilers, don't collide)
typedef typename std::make_signed<size_t>::type llssize;
#if LL_WINDOWS
// https://docs.microsoft.com/en-us/cpp/build/reference/zc-wchar-t-wchar-t-is-native-type
// https://docs.microsoft.com/en-us/cpp/cpp/fundamental-types-cpp
@ -45,7 +52,7 @@ typedef unsigned int U32;
// The version of clang available with VS 2019 also defines wchar_t as __wchar_t
// which is also 16 bits.
// In any case, llwchar should be a UTF-32 type.
typedef U32 llwchar;
typedef U32 llwchar;
#else
typedef wchar_t llwchar;
// What we'd actually want is a simple module-scope 'if constexpr' to test
@ -76,7 +83,7 @@ typedef double F64;
typedef S32 BOOL;
typedef U8 KEY;
typedef U32 MASK;
typedef U32 TPACKETID;
typedef U32 TPACKETID;
// Use #define instead of consts to avoid conversion headaches
#define S8_MAX (SCHAR_MAX)
@ -118,4 +125,95 @@ typedef U8 LLPCode;
typedef int intptr_t;
#endif
/*****************************************************************************
* Narrowing
*****************************************************************************/
/**
* narrow() is used to cast a wider type to a narrower type with validation.
*
* In many cases we take the size() of a container and try to pass it to an
* S32 or a U32 parameter. We used to be able to assume that the size of
* anything we could fit into memory could be expressed as a 32-bit int. With
* 64-bit viewers, though, size_t as returned by size() and length() and so
* forth is 64 bits, and the compiler is unhappy about stuffing such values
* into 32-bit types.
*
* It works to force the compiler to truncate, e.g. static_cast<S32>(len) or
* S32(len) or (S32)len, but we can do better.
*
* For:
* @code
* std::vector<Object> container;
* void somefunc(S32 size);
* @endcode
* call:
* @code
* somefunc(narrow(container.size()));
* @endcode
*
* narrow() truncates but, in RelWithDebInfo builds, it validates (using
* assert()) that the passed value can validly be expressed by the destination
* type.
*/
// narrow_holder is a struct that accepts the passed value as its original
// type and provides templated conversion functions to other types. Once we're
// building with compilers that support Class Template Argument Deduction, we
// can rename this class template 'narrow' and eliminate the narrow() factory
// function below.
template <typename FROM>
class narrow_holder
{
private:
FROM mValue;
public:
narrow_holder(FROM value): mValue(value) {}
/*---------------------- Narrowing unsigned to signed ----------------------*/
template <typename TO,
typename std::enable_if<std::is_unsigned<FROM>::value &&
std::is_signed<TO>::value,
bool>::type = true>
inline
operator TO() const
{
// The reason we skip the
// assert(value >= std::numeric_limits<TO>::lowest());
// in the overload below is that to perform the above comparison, the
// compiler promotes the signed lowest() to the unsigned FROM type,
// making it hugely positive -- so a reasonable 'value' will always
// fail the assert().
assert(mValue <= std::numeric_limits<TO>::max());
return static_cast<TO>(mValue);
}
/*----------------------- Narrowing all other cases ------------------------*/
template <typename TO,
typename std::enable_if<! (std::is_unsigned<FROM>::value &&
std::is_signed<TO>::value),
bool>::type = true>
inline
operator TO() const
{
// two different assert()s so we can tell which condition failed
assert(mValue <= std::numeric_limits<TO>::max());
// Funny, with floating point types min() is "positive epsilon" rather
// than "largest negative" -- that's lowest().
assert(mValue >= std::numeric_limits<TO>::lowest());
// Do we really expect to use this with floating point types?
// If so, does it matter if a very small value truncates to zero?
//assert(fabs(mValue) >= std::numeric_limits<TO>::min());
return static_cast<TO>(mValue);
}
};
/// narrow() factory function returns a narrow_holder<FROM>(), which can be
/// implicitly converted to the target type.
template <typename FROM>
inline
narrow_holder<FROM> narrow(FROM value)
{
return { value };
}
#endif