simplest “safe” integers

43
Simplest “Safe” Integers 1 2

Upload: others

Post on 24-Feb-2022

18 views

Category:

Documents


0 download

TRANSCRIPT

Simplest “Safe” Integers

1

2

Simplest Safe IntegersC++Now 2021-05-07

Peter [email protected]

@PeterSommerlad

Slides:

https://github.com/PeterSommerlad/talks_public/

3

Motivation

4 . 1

Why not built-in integers?

4 . 2

Why not built-in integers?undefined behavior on signed overflow

4 . 2

Why not built-in integers?undefined behavior on signed overflowintegral promotion is a curse

4 . 2

Why not built-in integers?undefined behavior on signed overflowintegral promotion is a curse

promotion can change signednessunsigned short x{0xffffu};x * x /* UB on 32bit */;

4 . 2

Why not built-in integers?undefined behavior on signed overflowintegral promotion is a curse

promotion can change signednessunsigned short x{0xffffu};x * x /* UB on 32bit */;

implicit conversion

4 . 2

Why not built-in integers?undefined behavior on signed overflowintegral promotion is a curse

promotion can change signednessunsigned short x{0xffffu};x * x /* UB on 32bit */;

implicit conversionsilent truncation

4 . 2

Why not built-in integers?undefined behavior on signed overflowintegral promotion is a curse

promotion can change signednessunsigned short x{0xffffu};x * x /* UB on 32bit */;

implicit conversionsilent truncationtoo many operations allowed (e.g., shifts for signed)

4 . 2

Why not built-in integers?undefined behavior on signed overflowintegral promotion is a curse

promotion can change signednessunsigned short x{0xffffu};x * x /* UB on 32bit */;

implicit conversionsilent truncationtoo many operations allowed (e.g., shifts for signed)mixing signed and unsigned allowed

4 . 2

Why not built-in integers?undefined behavior on signed overflowintegral promotion is a curse

promotion can change signednessunsigned short x{0xffffu};x * x /* UB on 32bit */;

implicit conversionsilent truncationtoo many operations allowed (e.g., shifts for signed)mixing signed and unsigned allowedimplementation-defined ranges <cstdint>

4 . 2

Why not built-in integers?undefined behavior on signed overflowintegral promotion is a curse

promotion can change signednessunsigned short x{0xffffu};x * x /* UB on 32bit */;

implicit conversionsilent truncationtoo many operations allowed (e.g., shifts for signed)mixing signed and unsigned allowedimplementation-defined ranges <cstdint>(some) developers expect wrapping 4 . 2

Undefined Behavior std::cout << 65535 * 32768 << '\n';// prints: 2147450880 std::cout << 65536 * 32768 << '\n';// prints: ? std::cout << std::numeric_limits<int>::is_modulo << '\n';// prints: false

4 . 3

2147450880 -2147483648 false

Speaker notes

void testUBforint() { std::ostringstream out{}; out << 65535 * 32768 << '\n'; // prints: 2147450880#pragma GCC diagnostic push#pragma GCC diagnostic ignored "-Woverflow" out << 65536 * 32768 << '\n'; //../src/Test.cpp:421:18: error: integer overflow in expression of type 'int' results in '-2147483648' [-Werror=overflow]#pragma GCC diagnostic pop // prints: ? out << std::boolalpha << std::numeric_limits<int>::is_modulo << '\n'; ASSERT_EQUAL("2147450880\n-2147483648\nfalse\n",out.str());}

No Undefined Behavior (unsigned) std::cout << 65535u * 32768u << '\n';// prints: 2147450880 std::cout << 65536u * 32768u << '\n';// prints: 2147483648 std::cout << 65536u * 32768u * 2u << '\n';// prints: ? std::cout << std::numeric_limits<unsigned>::is_modulo << '\n';// prints: true

4 . 4

Speaker notes

void testNoUBforunsigned() { std::ostringstream out{}; out << 65535u * 32768u << '\n'; // prints: 2147450880 out << 65536u * 32768u << '\n'; // prints: 2147483648

out << 65536u * 32768u * 2u << '\n'; // prints: ? out << std::boolalpha << std::numeric_limits<unsigned>::is_modulo << '\n'; ASSERT_EQUAL("2147450880\n2147483648\n0\ntrue\n",out.str());}

Where are non-UB integers useful?regulated domains, e.g., safety critical

!"#$%& ✈ ()*+MISRA-C++

recommend avoiding implementation-defined typesuse <cstdint> or similar aliases

try to prevent undefined behaviortry to guard against developer confusion

4 . 5

What to achieve?

4 . 6

What to achieve?mimic MISRA-C++ built-in types restrictions

4 . 6

What to achieve?mimic MISRA-C++ built-in types restrictions

i.e., limit bit operations to unsigned types

4 . 6

What to achieve?mimic MISRA-C++ built-in types restrictions

i.e., limit bit operations to unsigned typescode violating these restrictions doesn’t compile

4 . 6

What to achieve?mimic MISRA-C++ built-in types restrictions

i.e., limit bit operations to unsigned typescode violating these restrictions doesn’t compile

no need for static analyzers wrt these rules

4 . 6

What to achieve?mimic MISRA-C++ built-in types restrictions

i.e., limit bit operations to unsigned typescode violating these restrictions doesn’t compile

no need for static analyzers wrt these rulesno mixing of integers with character types

4 . 6

What to achieve?mimic MISRA-C++ built-in types restrictions

i.e., limit bit operations to unsigned typescode violating these restrictions doesn’t compile

no need for static analyzers wrt these rulesno mixing of integers with character typeschar char8_t char16_t char32_t

4 . 6

What to achieve?mimic MISRA-C++ built-in types restrictions

i.e., limit bit operations to unsigned typescode violating these restrictions doesn’t compile

no need for static analyzers wrt these rulesno mixing of integers with character typeschar char8_t char16_t char32_t

no promotion, and no changing of signedness

4 . 6

What to achieve?mimic MISRA-C++ built-in types restrictions

i.e., limit bit operations to unsigned typescode violating these restrictions doesn’t compile

no need for static analyzers wrt these rulesno mixing of integers with character typeschar char8_t char16_t char32_t

no promotion, and no changing of signednessno signed integer overflow UB (but wrapping)

4 . 6

What to achieve?mimic MISRA-C++ built-in types restrictions

i.e., limit bit operations to unsigned typescode violating these restrictions doesn’t compile

no need for static analyzers wrt these rulesno mixing of integers with character typeschar char8_t char16_t char32_t

no promotion, and no changing of signednessno signed integer overflow UB (but wrapping)no silent conversions

4 . 6

What to achieve?mimic MISRA-C++ built-in types restrictions

i.e., limit bit operations to unsigned typescode violating these restrictions doesn’t compile

no need for static analyzers wrt these rulesno mixing of integers with character typeschar char8_t char16_t char32_t

no promotion, and no changing of signednessno signed integer overflow UB (but wrapping)no silent conversions

except for widening of same signedness

4 . 6

What to achieve?mimic MISRA-C++ built-in types restrictions

i.e., limit bit operations to unsigned typescode violating these restrictions doesn’t compile

no need for static analyzers wrt these rulesno mixing of integers with character typeschar char8_t char16_t char32_t

no promotion, and no changing of signednessno signed integer overflow UB (but wrapping)no silent conversions

except for widening of same signednessexact same code generation (almost)

4 . 6

Enums as Integerreplacement

5 . 1

Limitations of my enum approachcannot have implicit conversionneeds named conversion functions for checksdivision by zero, shift UB ?

throws exception (macro controlled)asserts (no exceptions)returns 0 (NDEBUG)

5 . 2

enum for wrapping integersenum class types (like std::byte)operator overloadingconcepts constrained operatorsconsteval user-defined-literal operators

allow to implement wrapping, non-promoting integers:

Simple Safe Integers psssafeint.h

5 . 3

using enums as integers// unsigned enum class ui8 : std::uint8_t { tag_to_prevent_mixing_other_enums };enum class ui16: std::uint16_t{ tag_to_prevent_mixing_other_enums };enum class ui32: std::uint32_t{ tag_to_prevent_mixing_other_enums };enum class ui64: std::uint64_t{ tag_to_prevent_mixing_other_enums };// signedenum class si8 : std::int8_t { tag_to_prevent_mixing_other_enums };enum class si16: std::int16_t{ tag_to_prevent_mixing_other_enums };enum class si32: std::int32_t{ tag_to_prevent_mixing_other_enums };enum class si64: std::int64_t{ tag_to_prevent_mixing_other_enums };

5 . 4

User-defined Literals (UDL)inline namespace literals {constevalui16 operator""_ui16(unsigned long long val) { if (val <= std::numeric_limits<std::underlying_type_t<ui16>>::max())

{ return ui16(val); } else { throw "integral constant too large"; // trigger compile-time error }}// etc...

5 . 5

Using UDLsusing namespace psssint::literals; // required for UDLs

void ui16intExists() { using psssint::ui16; auto large=0xff00_ui16; //0x10000_ui16; // compile error //ui16{0xfffff}; // narrowing detection ASSERT_EQUAL(ui16{0xff00u},large);}

5 . 6

Traits and Conceptstemplate<typename T>using plain = std::remove_cvref_t<T>; // just a shorthandtemplate<typename T>concept an_enum = std::is_enum_v<plain<T>>;// from C++23template<an_enum T>constexpr boolis_scoped_enum_v = !std::is_convertible_v<T,

std::underlying_type_t<T>>;template<typename T>concept a_scoped_enum = is_scoped_enum_v<T>;

is_ for traits, a_/an_ for concepts

5 . 7

std::is_scoped_enum will become available in C++23

I currently prefer is_ prefix for bool traits and a_/an_ prefix for corresponding concept

Speaker notes

“Detection Idiom” with Concepttemplate<typename T>constexpr boolis_safeint_v = false;template<a_scoped_enum E>constexpr boolis_safeint_v<E> = requires { E{} == E::tag_to_prevent_mixing_other_enums; } ;template<typename E>concept a_safeint = is_safeint_v<E>;

5 . 8

Testing Detection Idiomnamespace _testing {using namespace psssint;static_assert(is_safeint_v<ui8>);static_assert(is_safeint_v<ui16>);static_assert(is_safeint_v<ui32>);static_assert(is_safeint_v<ui64>);static_assert(is_safeint_v<si8>);static_assert(is_safeint_v<si16>);static_assert(is_safeint_v<si32>);static_assert(is_safeint_v<si64>);enum class enum4test{};static_assert(!is_safeint_v<enum4test>);static_assert(!is_safeint_v<std::byte>);static_assert(!detail_::is_safeint_v<int>);

5 . 9

Meta Programming for Promotiontemplate<typename E> // ULT = underlying typeusing ULT=std::conditional_t<std::is_enum_v<plain<E>>, std::underlying_type_t<plain<E>>, plain<E>>;template<typename E>using promoted_t = // will promote keeping signedness std::conditional_t<(sizeof(ULT<E>) < sizeof(int)) , std::conditional_t<std::is_unsigned_v<ULT<E>> , unsigned , int > , ULT<E>>;template<a_safeint E>constexpr autoto_int(E val) noexcept { // promote keeping signedness return static_cast<promoted_t<E>>(val);}

5 . 10

Testing Same-sign Promotionstatic_assert(std::is_same_v<unsigned,decltype(to_int(1_ui8)+1)>);static_assert(std::is_same_v<unsigned,decltype(to_int(2_ui16)+1)>);static_assert(std::is_same_v<int,decltype(to_int(1_si8))>);static_assert(std::is_same_v<int,decltype(to_int(2_si16))>);

5 . 11

Concept for limiting integral (bad)template<std::integral T>constexpr boolis_integer_v = std::is_same_v<uint8_t , T> || std::is_same_v<uint16_t, T> || std::is_same_v<uint32_t, T> || std::is_same_v<uint64_t, T> || std::is_same_v<int8_t , T> || std::is_same_v<int16_t , T> || std::is_same_v<int32_t , T> || std::is_same_v<int64_t , T>;

template<typename T>concept an_integer = is_integer_v<T>;

What is bad?

5 . 12

is_integer_v failuretypedef __signed char int8_t; // from <cstdint>typedef short int16_t;typedef int int32_t;typedef long long int64_t;typedef unsigned char uint8_t;typedef unsigned short uint16_t;typedef unsigned int uint32_t;typedef unsigned long long uint64_t;

5 . 13

is_integer_v failuretypedef __signed char int8_t; // from <cstdint>typedef short int16_t;typedef int int32_t;typedef long long int64_t;typedef unsigned char uint8_t;typedef unsigned short uint16_t;typedef unsigned int uint32_t;typedef unsigned long long uint64_t;

char < short < int <long < long long

5 . 13

is_integer_v failuretypedef __signed char int8_t; // from <cstdint>typedef short int16_t;typedef int int32_t;typedef long long int64_t;typedef unsigned char uint8_t;typedef unsigned short uint16_t;typedef unsigned int uint32_t;typedef unsigned long long uint64_t;

char < short < int <long < long long

long is missing…

5 . 13

look for same range insteadtemplate<typename INT, typename TESTED>constexpr boolis_compatible_integer_v = std::is_same_v<TESTED,INT> || ( std::is_integral_v<TESTED> && !std::is_same_v<bool,TESTED> // exclude bool && !is_chartype_v<TESTED> // exclude characters && (std::is_unsigned_v<INT> == std::is_unsigned_v<TESTED>) && std::numeric_limits<TESTED>::max() == std::numeric_limits<INT>::max() );

template<typename INT, typename TESTED>constexpr boolis_similar_v=is_compatible_integer_v<INT,TESTED>;

5 . 14

character types are not integersMISRA-C++ excludes Character types fromarithmetic – requires deviation or library usage

template<typename CHAR>constexpr boolis_chartype_v = std::is_same_v<char, CHAR> || std::is_same_v<wchar_t, CHAR>#ifdef __cpp_char8_t || std::is_same_v<char8_t, CHAR>#endif || std::is_same_v<char16_t,CHAR> || std::is_same_v<char32_t,CHAR> ;

5 . 15

__cpp_char8_t is the feature test macro for the type char8_t introduced with C++20

Speaker notes

Concept for limiting integral (good)template<typename TESTED>constexpr boolis_known_integer_v = is_similar_v<std::uint8_t, TESTED> || is_similar_v<std::uint16_t, TESTED> || is_similar_v<std::uint32_t, TESTED> || is_similar_v<std::uint64_t, TESTED> || is_similar_v<std::int8_t, TESTED> || is_similar_v<std::int16_t, TESTED> || is_similar_v<std::int32_t, TESTED> || is_similar_v<std::int64_t, TESTED>;

// deliberately not std::integral, because of bool and characters!template<typename T>concept an_integer = detail_::is_known_integer_v<T>;

5 . 16

Meta Programming for Conversiontemplate<an_integer T>constexpr autofrom_int(T val) { using detail_::is_similar_v; using std::conditional_t; struct cannot_convert_integer{}; using result_t = conditional_t<is_similar_v<std::uint8_t,T>, ui8, conditional_t<is_similar_v<std::uint16_t,T>, ui16, conditional_t<is_similar_v<std::uint32_t,T>, ui32, conditional_t<is_similar_v<std::uint64_t,T>, ui64, conditional_t<is_similar_v<std::int8_t,T>, si8, conditional_t<is_similar_v<std::int16_t,T>, si16, conditional_t<is_similar_v<std::int32_t,T>, si32, conditional_t<is_similar_v<std::int64_t,T>, si64,

cannot_convert_integer>>>>>>>>; return static_cast<result_t>(val);}

5 . 17

The branch with cannot_convert_integer can never occur due to the limitation by the concept an_integer,however it is required, because we cannot leave out a template argument from conditional_t. void wouldhave been an option, but that would have created worse error messages.

Speaker notes

Directed Conversion (overengineered?)template<a_safeint TO, an_integer FROM>constexpr autofrom_int_to(FROM val) { using result_t = TO; using ultr = std::underlying_type_t<result_t>; if constexpr(std::is_unsigned_v<ultr>){ if ( val >= FROM{} && val <= std::numeric_limits<ultr>::max()) { return static_cast<result_t>(val); } else { throw "integral out of range"; } } else { if (val <= std::numeric_limits<ultr>::max() && val >= std::numeric_limits<ultr>::min()) { return static_cast<result_t>(val); } else { throw "integral out of range"; } }}

5 . 18

This is a deliberate cast of any sane integer type to one of our safe integers. the concepts ensure no non-sanctioned conversions are attempted, but this way, one can use an int and “cast” it to ui16, for example.

I still have to try, if I would like to actually have that, or if I need to use my magic assert macro ps_assert andreturn zero in case the conversion is invalid with NDEBUG production mode or fail at compile time.

Speaker notes

Testing from_int Conversionstatic_assert(1_ui8 == from_int(uint8_t(1)));static_assert(42_si8 == from_int_to<si8>(42));//static_assert(32_ui8 == from_int(' ')); // does not compile//static_assert(1_ui8 == from_int_to<ui8>(true)); // does not compile void checkedFromInt(){ using namespace psssint; ASSERT_THROWS(from_int_to<ui8>(2400u), char const *);}

5 . 19

from_int compiles?template<typename FROM, typename=void>constexpr boolfrom_int_compiles=false;

template<typename FROM>constexpr boolfrom_int_compiles<FROM, std::void_t<decltype(psssint::from_int(FROM{}))>> = true;//...static_assert(from_int_compiles<long>); // detect long bugstatic_assert(! from_int_compiles<bool>);static_assert(! from_int_compiles<char>);

5 . 20

I implemented the from_int_comiles testing facility to recognize the from long conversion bug.

Speaker notes

Output Operatortemplate<a_safeint E>std::ostream& operator<<(std::ostream &out, E value){ out << to_int(value); return out;}

concept a_safeint prevents using it for other types

std::ostream& operator<<(std::ostream &out, a_safeint auto value){ out << to_int(value); return out;} // concept short-hand notation

I prefer template<a_concept T> over (a_concept auto)5 . 21

Arithmetic Operatorstemplate<a_safeint E, a_safeint F>constexpr autooperator+(E l, F r) noexceptrequires same_sign<E,F> { using result_t=std::conditional_t<sizeof(E)>=sizeof(F),E,F>; return static_cast<result_t>( static_cast<ULT<result_t>>( to_uint(l) + // use unsigned op to prevent signed overflow, but wrap. to_uint(r) ) );}

5 . 22

Testing code generation on godbolt…template<typename INT>struct operations { operations(std::initializer_list<INT> seedvalues):values{seedvalues}{}; std::vector<INT> values;

INT sum() const { return std::accumulate(begin(values),end(values),INT()); }};

std::initializer_list<int8_t> i8_seed{1,1,2,3,5,8,13,21,34,55,89};std::initializer_list<psssint::si8>

si8_seed{1_si8,1_si8,2_si8,3_si8,5_si8,8_si8,13_si8,21_si8,34_si8,55_si8,89_si8};

auto sum(operations<int8_t> const &ops){ return ops.sum();}

auto sum(operations<si8> const &ops){ return ops.sum();}

5 . 23

Code generation identical (e.g. RISC-V C++17)

sum(operations<signed char> const&): # @sum(operations<signed char> const&)

lw a1, 0(a0) lw a0, 4(a0) beq a1, a0, .LBB0_3 mv a2, zero.LBB0_2: # =>This Inner Loop Header:

Depth=1 lb a3, 0(a1) addi a1, a1, 1 add a2, a2, a3 bne a0, a1, .LBB0_2 j .LBB0_4.LBB0_3: mv a2, zero.LBB0_4: slli a0, a2, 24 srai a0, a0, 24 ret

sum(operations<psssint::si8> const&): # @sum(operations<psssint::si8> const&)

lw a1, 0(a0) lw a0, 4(a0) beq a1, a0, .LBB1_3 mv a2, zero.LBB1_2: # =>This Inner Loop Header:

Depth=1 lb a3, 0(a1) addi a1, a1, 1 add a2, a2, a3 bne a0, a1, .LBB1_2 j .LBB1_4.LBB1_3: mv a2, zero.LBB1_4: slli a0, a2, 24 srai a0, a0, 24 ret

https://godbolt.org/z/dMGovTnMs

5 . 24

Suppressing all UB

6 . 1

Operators that might fail - no UB

1. throw exception - requires exception support2. assert() - disabled with NDEBUG3. return a wrong default result

What to do with operators that can fail?Division/Modulo by zero

Left-shift, right-shift by too many bits

6 . 2

it is highly desirable to detect anomalies, at least in test mode, so returning a wrong value should be just the meansof last resort in production mode (NDEBUG).

Speaker notes

Dedicated assertion with default return#ifdef NDEBUG /* case 3 */ #define ps_assert(default_value, cond) \ if (std::is_constant_evaluated()) {\ if (not (cond)) throw(#cond); /* compile error */\ } else {\ if (not (cond) ) return (default_value);/* last resort avoid UB */\ } #define NOEXCEPT_WITH_THROWING_ASSERTS noexcept#else #ifdef PS_ASSERT_THROWS /* case 1 */ #define ps_assert(default_value, cond) ((cond)?true: throw(#cond)) #define NOEXCEPT_WITH_THROWING_ASSERTS noexcept(false) #else /* case 2 */ #include <cassert> #define ps_assert(default_value, cond) assert(cond) #define NOEXCEPT_WITH_THROWING_ASSERTS noexcept #endif#endif

6 . 3

ps_assert(0,condition)template<a_safeint E, a_safeint F>constexpr autooperator/(E l, F r) NOEXCEPT_WITH_THROWING_ASSERTS // <--requires same_signedness<E,F>{ using result_t=std::conditional_t<sizeof(E)>=sizeof(F),E,F>; ps_assert(result_t{}, r != F{} && " division by zero"); // <-- return static_cast<result_t>( static_cast<detail_::ULT<result_t>>( to_uint(l) / // use unsigned op to prevent signed overflow, but wrap. to_uint(r) ) );}

returning zero on div by zero is scientifically substantiated

6 . 4

integer types do not have NaN to support a special return value.

There is scientific support to return zero:

J.A. Bergstra, J.V. Tucker : Division Safe Calculation in Totalised Fields; DOI 10.1007/s00224-007-9035-4;TheoryComput Syst (2008) 43: 410–424

Speaker notes

testing assertion handlingvoid thisIsATestForZeroReturnAssertWithNDEBUGTest() { constexpr auto divisor = 0_si8; auto divident = 1_si8;#ifdef NDEBUG /* case 3 */ ASSERT_EQUAL(0_si8,divident/divisor); ASSERT_EQUAL(0_si8,divident /= divisor );#else #ifdef PS_ASSERT_THROWS /* case 1 */ ASSERT_THROWS(divident/divisor, char const *); #else /* case 2 */ #ifdef PS_TEST_TRAP /* CUTE cannot check for crashes */ ASSERTM("cannot test trapping without NDEBUG set, change this to true to check for

assert() behavior ",false); divident/divisor; // Assertion failed: (r != F{} && " division by zero"), function

operator/, file ../src/psssafeint.h, line 517. #endif ASSERT(divident != divisor); // dummy to prevent compile warning #endif#endif //constexpr auto should_not_compile = 1_si8 / divisor; in Test.cpp

6 . 5

Checking for compilabilitynamespace compile_checks {using namespace psssint;template<auto ...value>using consume_value = void;//...check_does_compile(not, si8 , << )check_does_compile( , ui8 , << )check_does_compile(not, si8 , >> )check_does_compile( , ui8 , >> )check_does_compile(not, ui8 , + (1_ui8 << 010_ui8) + ) // too wide shiftcheck_does_compile( , ui8 , + (1_ui8 << 7_ui8) + ) // not too wide shiftcheck_does_compile(not, ui8 , + (0x80_ui8 >> 010_ui8) + ) // too wide shiftcheck_does_compile( , ui8 , + (0x80_ui8 >> 7_ui8) + ) // not too wide shiftcheck_does_compile(not, ui8 , % ) // modulo 0check_does_compile(not, si8 , / ) // div 0check_does_compile(not, si8 , % ) // modulo not workingcheck_does_compile(not, ui8 , / ) // div 0

6 . 6

Macro to the rescue#define concat_line_impl(A, B) A##_##B#define concat_line(A, B) concat_line_impl(A,B)

#define check_does_compile(NOT, FROM, oper) \namespace concat_line(NOT##_test, __LINE__) { \ template<typename T, typename=void>\ constexpr bool\ expression_compiles=false;\template<typename T> \constexpr bool \expression_compiles<T, consume_value<(T{} oper T{})> > = true;\static_assert(NOT expression_compiles<FROM>, \ "should " #NOT " compile: " #FROM "{}" #oper #FROM "{}");\} // namespace tag

6 . 7

what is actually checkedcheck_does_compile(not, si8 , << ) // si8{} << si8{} check_does_compile( , ui8 , << ) // ui8{} << ui8{}check_does_compile(not, si8 , >> ) // si8{} >> si8{} check_does_compile( , ui8 , >> ) // ui8{} >> ui8{}check_does_compile(not, si8 , % ) // si8{} % si8{}

6 . 8

checking div by zero at compile timetemplate<a_safeint E, a_safeint F>constexpr autooperator/(E l, F r) NOEXCEPT_WITH_THROWING_ASSERTSrequires same_signedness<E,F>{ using result_t=std::conditional_t<sizeof(E)>=sizeof(F),E,F>; ps_assert(result_t{}, r != F{} && " division by zero"); return static_cast<result_t>( static_cast<detail_::ULT<result_t>>( to_uint(l) / // use unsigned op to prevent signed overflow, but wrap. to_uint(r) ) );}check_does_compile(not, si8 , / ) // div 0check_does_compile(not, ui8 , / ) // div 0

6 . 9

checking with compile time valuescheck_does_compile(not, ui8 , + (1_ui8 << 010_ui8) + ) // too wide shiftcheck_does_compile( , ui8 , + (1_ui8 << 7_ui8) + ) // not too wide shiftcheck_does_compile(not, ui8 , + (0x80_ui8 >> 010_ui8) + ) // too wide shiftcheck_does_compile( , ui8 , + (0x80_ui8 >> 7_ui8) + ) // not too wide shiftcheck_does_compile(not, ui8 , % ) // modulo 0check_does_compile(not, si8 , / ) // div 0check_does_compile(not, si8 , % ) // modulo not workingcheck_does_compile(not, ui8 , / ) // div 0check_does_compile( , ui8 , +( 1_ui8 / 1_ui8)+ ) // divcheck_does_compile( , ui8 , +( 11_ui8 % 3_ui8)+ ) // modcheck_does_compile(not, ui8 , + 1_si8 + ) // mixed signednesscheck_does_compile( , ui8 , + 255_ui8 + 1_ui8 + )

6 . 10

Improved from_int_totemplate<a_safeint TO, an_integer FROM>constexpr autofrom_int_to(FROM val) NOEXCEPT_WITH_THROWING_ASSERTS{ using result_t = TO; using ultr = std::underlying_type_t<result_t>; if constexpr(std::is_unsigned_v<ultr>){ ps_assert( result_t{}, (val >= FROM{} && val <= std::numeric_limits<ultr>::max())) ; return static_cast<result_t>(val); } else { ps_assert( result_t{}, (val <= std::numeric_limits<ultr>::max() && val >= std::numeric_limits<ultr>::min())); return static_cast<result_t>(val); }}

6 . 11

A bummercompiling with -DNDEBUG

../src/psssafeint.h: In instantiation of 'constexpr auto psssint::from_int_to(FROM) [with TO = psssint::si8; FROM = int]':../src/Test.cpp:146:44: required from here../src/psssafeint.h:22:24: warning: 'throw' will always call 'terminate' [-Wterminate] 22 | if (not (cond)) throw(#cond); /* compile error, but also gcc -Wterminate */\ | ^~~~~~~~~~~~../src/psssafeint.h:375:9: note: in expansion of macro 'ps_assert' 375 | ps_assert( result_t{}, (val <= std::numeric_limits<ultr>::max() && | ^~~~~~~~~

This is a bug in gcc’s -Wterminate handling, throwonly happens at compile time

6 . 12

ugly bummer remedy#ifdef NDEBUG#pragma GCC diagnostic push#if defined(__GNUG__) && !defined(__clang__)#pragma GCC diagnostic ignored "-Wterminate"#endif#endif ps_assert( result_t{}, (val >= FROM{} && val <= std::numeric_limits<ultr>::max())) ;#ifdef NDEBUG#pragma GCC diagnostic pop#endif

without excluding clang, clang complains about wrongwarning flag

6 . 13

sometimes -Werror is a burden, when compiler warnings produce false positives.

Speaker notes

std::numeric_limitstemplate<psssint::a_safeint type> // constrained struct numeric_limits<type> { using ult = psssint::detail_::ULT<type>; static constexpr bool is_specialized = true; static constexpr type min() noexcept { return type{numeric_limits<ult>::min()}; } static constexpr type max() noexcept { return type{numeric_limits<ult>::max()}; }// ... static constexpr int digits = numeric_limits<ult>::digits; static constexpr int digits10 = numeric_limits<ult>::digits10;//... static constexpr bool is_modulo = true; // even for signed static constexpr bool traps = false; // never signals};

6 . 14

C++17 branch

7 . 1

backporting to C++17replace all constrained templates with std::enable_if or other SFINAEstd::numeric_limits<> cannot be constrained,needs trampoline

template<>struct numeric_limits<psssint::si8> : psssint::detail_::numeric_limits<psssint::si8>{};

7 . 2

C++17 backport example#ifdef __cpp_conceptstemplate<a_safeint E>#elsetemplate<typename E,std::enable_if_t< detail_::is_safeint_v<E> && std::numeric_limits<E>::is_signed,bool> = true>#endifconstexpr Eoperator-(E l) noexcept#ifdef __cpp_conceptsrequires std::numeric_limits<E>::is_signed#endif{ return static_cast<E>(1u + ~to_uint(l));}

7 . 3

Alternative Libraries

Either of those might be better suited to your needsthan psssafeint.They detect and mitigate overflow and otherpotential UB in a configurable wayThey can give more accurate results

There might be more, but those are the ones I had a(cursory) look at.

boost/safe_numerics

CNL Compositional Numeric Library

7 . 4

Wrapping-up Simple Safe IntegersFree to use and downloadrequires C++20

a C++17 backport exists but is more uglyTarget audience:

embedded developerssafety critical systems developers

best if regular integer types are prevented by staticanalysis

https://github.com/PeterSommerlad/PSsimplesafeint

7 . 5

Questions & ContactPeter Sommerlad

[email protected]@PeterSommerlad

https://sommerlad.ch

PSsimplesafeint:

https://github.com/PeterSommerlad/PSsimplesafeint

8