Single header, type conversions library
Proper conversion between types is a tedious task.
- Some conversions result in a loss of data, implementation defined behavior or even undefined behavior (nasal deamons).
- Converting between types using casting operators
(type)
, does not convey a clear the intention of the programmer. Is the cast done, because the value is guaranteed to fit destination type? Is it done because truncating the data is expected? - Casting operators silence compiler warnings about conversions. If there is
a conversion
(uint8_t)var
, wherevar
isuint32_t
, and in future the code is changed, so that type ofvar
becomesfloat
, most likely the code where conversion occurs should be reviewed again, but it can be overlooked, because an expression which uses such casting operator usually does not produce warnings. - Manually writing conditions to check if value fits a given range can be error prone.
This library of helper functions was created to make dealing with these problems easier.
And let's be honest. The world just needs an another C single header library.
cast.h
is a
STB-style
single header library, because such libraries are easier to integrate into
projects.
Just drop in cast.h
directly into your project and create a .c
file with
the following contents:
// a translation unit where you want to implement the cast library
#define CAST_IMPLEMENTATION
#include "cast.h"
Once you've done that, you can include cast.h
in other translation units
and start using conversion functions.
// in other translation units where you want to use conversion functions
#include "cast.h"
For a more in depth explanation refer to https://github.com/nothings/stb/blob/master/docs/stb_howto.txt
The library provides two kinds of functions:
-
Error returning functions - these functions are in form of
try_{T'}_from_{U'}(T* dst, U src)
and they return an error on failure. These functions can be used in cases where value is not guaranteed to fit the destination type and you want to provide error handling for cases where it does not fit. -
Panic handler invoking functions - these functions are in form of
{T'}_from_{U'}(U src)
and they invoke the panic handler on failure. The default behavior of the panic handler is to callexit(1)
and crash the application. Such functions should be used in cases where conversion is expected to succeed and failure is a unexpected bug or security error, which shall lead to immediate program termination.
In order to make function conversion names shorter and more memorable,
the T'
and U'
are short versions of the type
T
and U
type names respectively. Here is a full list of supported types:
T
:uint8_t
,T'
:u8
T
:uint16_t
,T'
:u16
T
:uint32_t
,T'
:u32
T
:uint64_t
,T'
:u64
T
:int8_t
,T'
:i8
T
:int16_t
,T'
:i16
T
:int32_t
,T'
:i32
T
:int64_t
,T'
:i64
T
:short
,T'
:short
T
:int
,T'
:int
T
:long
,T'
:long
T
:long long
,T'
:llong
T
:unsigned short
,T'
:ushort
T
:unsigned int
,T'
:uint
T
:unsigned long
,T'
:ulong
T
:unsigned long long
,T'
:ullong
T
:bool
,T'
:bool
T
:float
,T'
:float
T
:double
,T'
:double
T
:size_t
,T'
:size
T
:ptrdiff_t
,T'
:ptrdiff
T
:uintptr_t
,T'
:uptr
To convert from U
type to T
type use:
// Try to convert `src` of type U to type T stored in `dst` and return
// 0 on success and non-zero value on failure. In the case of error `dst`
// is left unmodified
int try_{T'}_from_{U'}(T *dst, U src);
Example:
void foo(int x, short y)
{
uint32_t dst_u32 = 0U;
int err = try_u32_from_int(&dst_u32, x);
if (err) {
// Handle error
}
printf("%"PRNu32"\n", dst_u32);
size_t count = 0U;
int err = try_size_from_short(&count, y);
if (err) {
// Handle error
}
printf("%zu\n", count);
}
To convert from U
type to T
, without error handling:
// Convert `src` of type U to type T and return converted value
// If the conversion is not possible to do without loss of data / precision
// then the panic handler is invoked (default behaviors is to crash the application).
T {T'}_from_{U'}(U src);
Example:
void print_numbers(int *numbers, int count)
{
if (!numbers)
return;
// Program will crash if count is < 0
for (size_t i = 0; i < size_from_int(count); ++i)
printf("%d\n", numbers[i]);
}
You can overwrite the default panic handler (which just calls exit(1)
),
by defining CAST_CUSTOM_PANIC
constant before including cast.h
.
#define CAST_IMPLEMENTATION
#define CAST_CUSTOM_PANIC
#include "cast.h"
void cast_panic_impl(const char *msg, ...)
{
printf("your custom panic handler\n");
exit(1);
}
If you define a panic handler that does not exit the application,
then {T'}_from_{U'}()
when error occurrs those functions will return zero
initialized values instead of crashing application.
Casting integer types will check if destination type can represent the source value. It will return error or invoke panic handler if the source value can't be represented by destination type. This also means that an error will be triggered when you try to convert a negative number to unsigned type.
Example of casting unsinged types to unsigned types:
// Will panic if x can't fit size_t
size_t count = size_from_u32(x);
// Will return error if x can't fit size_t
if (try_size_from_u32(&count, x)) {
// handle error
}
Example of casting singed types to unsigned types:
// Will panic if x is too large or is negative
size_t count = size_from_int(x);
// Will return error if x is too large or is negative
if (try_size_from_int(&count, x)) {
// handle error
}
Example of casting unsinged types to signed types:
// Will panic if x is too large
short number = short_from_size(x);
// Will return error if x is too large
if (try_short_from_size(&number, x)) {
// handle error
}
Example of casting singed types to signed types:
// Will panic if x is too large or too small
int16_t count = i16_from_int(x);
// Will return error if x is too large or too small
if (try_i16_from_int(&count, x)) {
// handle error
}
Casting an integer type to floating point type will only succeed if the integer type can be represented exactly by the the floating point type without any precision loss.
For example, this operation will succeed:
int64_t x = 1234;
float variable = float_from_int(x);
This operation however, will invoke the panic handler, because it results in unexpected precision loss
int64_t x = 0xFFFFFFFF; // 32 ones
float var = 0.0f;
// error, because float's mantissa has only 24 bits of precision (with implicit bit)
if (try_float_from_i64(&var, x)) {
// handle error
}
// panic handler will be invoked
var = float_from_int(x);
Note that there are integers larger than 0xFFFFFFFF
that can fit float
, for
example 0xF00000000
is a larger number, but it requires less bits to be accurately
represented by the float
type, because trailing zeros will be represented
by using larger exponent.
float ok = float_from_u64(0xF00000000ULL); // no error
Analoguous functions exists for double
and they allow more integers due
to the fact that double
has 54 bits of mantissa (with implicit bit).
double ok = double_from_u32(0xFFFFFFFFUL);
double also_ok = double_from_u64(0xFFFFFFFFFFFF0000ULL);
double not_ok = double_from_u64(0xFFFFFFFFFFFFFFFFULL); // error