Component of the Week #11: bslh_hash
- Summary:
Provide composable facilities for defining hash functions.
A hash table is a fundamental associative data structure that provides
efficient — O(1) — algorithms to look up the data associated with
a given value of its key type. This lookup depends on having a quality
hash function that (quickly) maps arbitrary values of the key type to
a fixed-width integral type (like std::size_t) with as few collisions
as possible.
The C++ standard library has, since C++11, provided hash table based containers
in the form of std::unordered_set and std::unordered_map (and their
corresponding multi-value alternatives). In C++23,
these expanded to included std::flat_map. BDE provides our own implementations
in the bsl namespace, such as
bslstl_unorderedset
and
bslstl_unorderedmap.
In addition, BDE has more hash table based associated containers like
bdlc_flathashmap
and
bdlcc_stripedunorderedmap.
All of these containers depend on being able to compute a hash value for
an instance of the key type. The standard computes hash values through the
use of std::hash<T>, which is a functor that may be specialized for
user-defined types. This approach, however, has drawbacks
— users are writing
code themselves in namespace std, they must implement a correct and performant
hashing algorithm themselves, and composing hashing operations in terms of
hashing each member is not supported by the interface.
BDE’s hashing system
package, which is used by bsl::hash, aims to solve all of these
problems.
bsl::hash computes the hash value of an object in three steps:
1) An instance of a hash algorithm is created (which at the time of this writing is, by default, an instance of bslh::WyHashIncrementalAlgorithm). This object will be used to accumulate a hash value efficiently.
2) The function
hashAppendis then invoked with two parameters — the hash algorithm and the object being hashed, to update the hash value recursively for every relevant part of the object being hashed.3) The final hash value that has accumulated in the hash algorithm object will be returned.
This hashing system separates concerns in certain key ways:
For any user-defined type, a function
hashAppendcan be defined that identifies the (salient) properties of that type relevant to computing a hash value for objects of that type. For a specific typeT, ahashAppendforTshould be a free function template in the same namespace or hidden friend, templated on the hash algorithm:class T { /* ... */ }; template <typename t_HASH_ALGORITHM> void hashAppend(t_HASH_ALGORITHM& hashAlg, const T& value);This function need only do one thing – recursively call
hashAppendfor each of the constituent and subobjects ofTthat are salient parts of the value of an object of typeT.
For any composite object,
hashAppendcan recursively be applied to all members that are part of the salient value of the object — i.e., exactly those members that participate in the determination of object equality done byoperator==.Raw bytes can be passed to update the hash algorithm directly by passing a pointer and size to the call operator of the hash algorithm.
For more information on implementing correct overloads of
hashAppend, see the discussion on implementing hashAppend in the package documentation for bslh.A hash algorithm can be selected, independently of the users implementation of
hashAppendfor their user-defined types. This allows the use of tested and vetted hash algorithms that typically provide an a good distribution of values, and avoid problems that may cause degenerate behavior in associative containers (such as not making use of all the bytes of the hash value).
By default
bslh::Hashwill useWyHash, which provides a good balance of performance and sensitivity to its inputs.Users may, for example, wish to perform hashing with an algorithm with different guarantees, such as bslh::SipHashAlgorithm) that provides better protections against malicious inputs.
Putting this to use is simple.
Let’s say you are writing a variation on a popular mining-themed
game and want to keep track of graffiti written on the faces of
voxels in your world. You begin
by modeling voxel locations
as a combination of a dimension and coordinates, while each face
of a voxel can be identified using an enum, both of which
you combine to make your Face type that represents any face
of any voxel in your universe:
namespace mine {
class Location {
// ...
public:
const bsl::string& getDimension() const;
const int getX() const;
const int getY() const;
const int getZ() const;
// ...
};
enum class Facing {
e_TOP, e_BOTTOM, e_NORTH, e_SOUTH, e_EAST, e_WEST
};
class Face {
// ...
public:
const Location& getLocation() const;
const Facing getFacing() const;
// ...
};
} // close namespace mine
Now you’d like to track, in an associative container, what has been written on each face:
bsl::unordered_map<mine::Face, bsl::string> allGraffiti;
The problem here, of course, is that mine::Face does not
support hashing. Solving that problem is easy, however,
by defining a hashAppend in the same namespace:
namespace mine {
template <typename t_HASH_ALGORITHM>
void hashAppend(t_HASH_ALGORITHM& hashAlg, const Face& face)
{
using bslh::hashAppend;
hashAppend(hashAlg, face.getLocation());
hashAppend(hashAlg, face.getFacing());
}
} // close namespace mine
Of course, this will depend on having hashAppend work for the members
we are recursively applying it to — an instance of another user-defined
type mine::Location and our enum, mine::Facing. For the Location
type we do exactly the same thing we did for Face and define an overload
for hashAppend:
namespace mine {
template <typename t_HASH_ALGORITHM>
void hashAppend(t_HASH_ALGORITHM& hashAlg, const Location& location)
{
using bslh::hashAppend;
hashAppend(hashAlg, location.getDimension());
hashAppend(hashAlg, location.getX());
hashAppend(hashAlg, location.getY());
hashAppend(hashAlg, location.getZ());
}
} // close namespace mine
For the enum, the int coordinate values, and even for bsl::string
we don’t need to do anythiing else — hashAppend already has the needed
overloads in the right namespaces (bsl for bsl::string and bslh
for the scalar types).
Now our bsl::unordered_map can be used to track all the graffiti we might
ever want to track:
void initializeGraffiti()
{
allGraffiti[ {{"overworld", 0, 40, 0}, Facing::e_TOP} ] = "Spawn Here";
allGraffiti[ {{"nether", 0, 0, 0}, Facing:e_SOUTH} ] = "Scary Place";
allGraffiti[ {{"end", 0, -40, 0}, Facing:e_BOTTOM} ] = "Fall Forever";
}
If you’d like to know more, check out the documentation for bslh_hash for details. There, and in the bslh package, you will find:
Other hash algorithms that can be used instead of the default.
The many BDE and
stdtypes that already have support forhashAppend.