<functional> Support
Description
The library provides a specialization of std::hash for every safe number type, so they can be used as keys in the standard unordered associative containers (std::unordered_set, std::unordered_map, and their multi-keyed variants).
The specialization extracts the underlying value and delegates to the standard hash of that underlying type, so a safe value hashes identically to the built-in value it wraps.
#include <boost/safe_numbers/functional.hpp>
template <boost::safe_numbers::detail::library_type T>
struct std::hash<T>
{
[[nodiscard]] auto operator()(const T& value) const noexcept -> std::size_t;
};
The specialization covers all safe number types: the safe integers (u8, u16, u32, u64, u128, i8, i16, i32, i64, i128), the safe floating-point types (f32, f64), and the bounded types (bounded_uint<Min, Max>, bounded_int<Min, Max>, bounded_float<Min, Max>).
Behavior
The call operator is noexcept and computes its result by casting the value to its underlying type and applying std::hash to that underlying value:
-
It is deterministic: two values that compare equal produce the same hash.
-
It agrees with the underlying hash: for the hardware-backed types,
std::hash<T>{}(value)equalsstd::hashof the corresponding built-in value. For example,std::hash<u32>{}(u32{x})equalsstd::hash<std::uint32_t>{}(x). -
For the 128-bit types (
u128,i128), the underlying value is hashed using the hash provided by the bundledboost::int128support rather than a standard built-in. -
For the floating-point types, hashing follows the underlying
std::hash<float>andstd::hash<double>semantics exactly, including the treatment of signed zeros. Becausef32andf64do not validate their value at construction (see Floating-Point Types), a value holding a NaN can still be hashed; its hash is whatever the standard library produces for that NaN.
Examples
std::hash specialization.// Copyright 2026 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt
#include <boost/safe_numbers/functional.hpp>
#include <boost/safe_numbers/unsigned_integers.hpp>
#include <boost/safe_numbers/signed_integers.hpp>
#include <boost/safe_numbers/floats.hpp>
#include <cstdint>
#include <functional>
#include <iostream>
#include <string>
#include <unordered_map>
#include <unordered_set>
int main()
{
using namespace boost::safe_numbers;
std::cout << std::boolalpha;
// Use a safe type as a key in an unordered container
std::unordered_set<u32> seen {};
seen.insert(u32{7});
seen.insert(u32{8});
std::cout << "seen.count(7) = " << seen.count(u32{7}) << '\n';
std::cout << "seen.size() = " << seen.size() << '\n';
std::cout << '\n';
// Safe types work as unordered_map keys too
std::unordered_map<i64, std::string> labels {};
labels[i64{-1}] = "minus one";
labels[i64{42}] = "the answer";
std::cout << "labels[-1] = " << labels[i64{-1}] << '\n';
std::cout << "labels[42] = " << labels[i64{42}] << '\n';
std::cout << '\n';
// The hash of a safe value matches the hash of the underlying built-in
const auto safe_hash {std::hash<u32>{}(u32{42})};
const auto raw_hash {std::hash<std::uint32_t>{}(42u)};
std::cout << "hash(u32{42}) == hash(uint32_t{42}) : " << (safe_hash == raw_hash) << '\n';
// Floating-point values hash exactly as the built-in does
const auto safe_f {std::hash<f64>{}(f64{3.14})};
const auto raw_f {std::hash<double>{}(3.14)};
std::cout << "hash(f64{3.14}) == hash(double{3.14}) : " << (safe_f == raw_f) << '\n';
return 0;
}
Output:
seen.count(7) = 1
seen.size() = 2
labels[-1] = minus one
labels[42] = the answer
hash(u32{42}) == hash(uint32_t{42}) : true
hash(f64{3.14}) == hash(double{3.14}) : true