Resolve the std/hashbrown conflict situation

Big diff, but it’s mostly just whitespace changes; ignore whitespace and
it’s much smaller, though still not as tiny as it could potentially be.

Essentially, this just duplicates everything for the hashbrown variant.

It’d be possible to use generic associated types to achieve this without
the duplication, but that depends on currently-unstable features, and is
probably slightly more painful to use anyway. I’ll keep the approach in
mind for a possible version 2, but for now this is the pragmatic route.
This commit is contained in:
Chris Morgan 2022-02-22 13:50:22 +11:00
parent e04b8b4d6e
commit 40e60cefd6
5 changed files with 606 additions and 606 deletions

View file

@ -7,6 +7,15 @@ being bigger than Id earlier intended.
- Fixed the broken `Extend` implementation added in 1.0.0-beta.1.
- Split the hashbrown implementation into a new module, `hashbrown`:
std and hashbrown can now coexist completely peacefully,
with `anymap::Map` being powered by `std::collections::hash_map`,
and `anymap::hashbrown::Map` being powered by `hashbrown::hash_map`.
The `raw_hash_map` alias, provided in 1.0.0-beta.1 because of the ambiguity
of what backed `anymap::Map`, is removed as superfluous and useless.
`RawMap` remains, despite not being *required*, as an ergonomic improvement.
With this, were back to proper completely additive Cargo features.
# 1.0.0-beta.1 (2022-01-25)
- Removed `anymap::any::Any` in favour of just plain `core::any::Any`, since its

View file

@ -11,6 +11,9 @@ categories = ["rust-patterns", "data-structures", "no-std"]
license = "BlueOak-1.0.0 OR MIT OR Apache-2.0"
include = ["/README.md", "/COPYING", "/CHANGELOG.md", "/src"]
[package.metadata.docs.rs]
all-features = true
[features]
default = ["std"]
std = []

View file

@ -41,22 +41,20 @@ assert_eq!(&*data.get::<Foo>().unwrap().str, "foot");
## Cargo features/dependencies/usage
Typical Cargo.toml usage:
Typical Cargo.toml usage, providing `anymap::AnyMap` *et al.* backed by `std::collections::HashMap`:
```toml
[dependencies]
anymap = "1.0.0-beta.1"
```
No-std usage, using `alloc` and the [hashbrown](https://rust-lang.github.io/hashbrown) crate instead of `std::collections::HashMap`:
No-std usage, providing `anymap::hashbrown::AnyMap` *et al.* (note the different path, required because Cargo features are additive) backed by `alloc` and the [hashbrown](https://rust-lang.github.io/hashbrown) crate:
```toml
[dependencies]
anymap = { version = "1.0.0-beta.1", default-features = false, features = ["hashbrown"] }
```
The `std` feature is enabled by default. The `hashbrown` feature overrides it. At least one of the two must be enabled.
**On stability:** hashbrown is still pre-1.0.0 and experiencing breaking changes. Because its useful for a small fraction of users, I am retaining it, but with *different compatibility guarantees to the typical SemVer ones*. Where possible, I will just widen the range for new releases of hashbrown, but if an incompatible change occurs, I may drop support for older versions of hashbrown with a bump to the *minor* part of the anymap version number (e.g. 1.1.0, 1.2.0). Iff youre using this feature, this is cause to *consider* using a tilde requirement like `"~1.0"` (or spell it out as `>=1, <1.1`).
## Unsafe code in this library

View file

@ -1,80 +1,70 @@
//! This crate provides a safe and convenient store for one value of each type.
//!
//! Your starting point is [`Map`]. It has an example.
//!
//! # Cargo features
//!
//! This crate has two independent features, each of which provides an implementation providing
//! types `Map`, `AnyMap`, `OccupiedEntry`, `VacantEntry`, `Entry` and `RawMap`:
//!
#![cfg_attr(feature = "std", doc = " - **std** (default, *enabled* in this build):")]
#![cfg_attr(not(feature = "std"), doc = " - **std** (default, *disabled* in this build):")]
//! an implementation using `std::collections::hash_map`, placed in the crate root
//! (e.g. `anymap::AnyMap`).
//!
#![cfg_attr(feature = "hashbrown", doc = " - **hashbrown** (optional; *enabled* in this build):")]
#![cfg_attr(not(feature = "hashbrown"), doc = " - **hashbrown** (optional; *disabled* in this build):")]
//! an implementation using `alloc` and `hashbrown::hash_map`, placed in a module `hashbrown`
//! (e.g. `anymap::hashbrown::AnyMap`).
#![warn(missing_docs, unused_results)]
#![cfg_attr(not(feature = "std"), no_std)]
use core::any::{Any, TypeId};
use core::convert::TryInto;
use core::hash::{Hasher, BuildHasherDefault};
use core::marker::PhantomData;
#[cfg(not(any(feature = "std", feature = "hashbrown")))]
compile_error!("anymap: you must enable the 'std' feature or the 'hashbrown' feature");
use core::hash::Hasher;
#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::boxed::Box;
use any::{Downcast, IntoBox};
pub use any::CloneAny;
#[cfg(all(feature = "std", not(feature = "hashbrown")))]
/// A re-export of [`std::collections::hash_map`] for raw access.
///
/// If the `hashbrown` feature gets enabled, this will become an export of `hashbrown::hash_map`.
///
/// As with [`RawMap`][crate::RawMap], this is exposed for compatibility reasons, since features
/// are supposed to be additive. This *is* imperfect, since the two modules are incompatible in a
/// few places (e.g. hashbrowns entry types have an extra generic parameter), but its close, and
/// much too useful to give up the whole concept.
pub use std::collections::hash_map as raw_hash_map;
#[cfg(feature = "hashbrown")]
/// A re-export of [`hashbrown::hash_map`] for raw access.
///
/// If the `hashbrown` feature was disabled, this would become an export of
/// `std::collections::hash_map`.
///
/// As with [`RawMap`][crate::RawMap], this is exposed for compatibility reasons, since features
/// are supposed to be additive. This *is* imperfect, since the two modules are incompatible in a
/// few places (e.g. hashbrowns entry types have an extra generic parameter), but its close, and
/// much too useful to give up the whole concept.
pub use hashbrown::hash_map as raw_hash_map;
use self::raw_hash_map::HashMap;
pub use crate::any::CloneAny;
mod any;
#[cfg(any(feature = "std", feature = "hashbrown"))]
macro_rules! everything {
($example_init:literal, $($parent:ident)::+ $(, $entry_generics:ty)?) => {
use core::any::{Any, TypeId};
use core::hash::BuildHasherDefault;
use core::marker::PhantomData;
#[cfg(not(feature = "std"))]
use alloc::boxed::Box;
use ::$($parent)::+::hash_map::{self, HashMap};
use crate::any::{Downcast, IntoBox};
/// Raw access to the underlying `HashMap`.
///
/// This is a public type alias because the underlying `HashMap` could be
/// `std::collections::HashMap` or `hashbrown::HashMap`, depending on the crate features enabled.
/// For that reason, you should refer to this type as `anymap::RawMap` rather than
/// `std::collections::HashMap` to avoid breakage if something else in your crate tree enables
/// hashbrown.
///
/// See also [`raw_hash_map`], an export of the corresponding `hash_map` module.
/// This alias is provided for convenience because of the ugly third generic parameter.
pub type RawMap<A> = HashMap<TypeId, Box<A>, BuildHasherDefault<TypeIdHasher>>;
/// A collection containing zero or one values for any given type and allowing convenient,
/// type-safe access to those values.
///
/// The type parameter `A` allows you to use a different value type; normally you will want it to
/// be `core::any::Any` (also known as `std::any::Any`), but there are other choices:
/// The type parameter `A` allows you to use a different value type; normally you will want
/// it to be `core::any::Any` (also known as `std::any::Any`), but there are other choices:
///
/// - If you want the entire map to be cloneable, use `CloneAny` instead of `Any`; with that, you
/// can only add types that implement `Clone` to the map.
/// - You can add on `+ Send` or `+ Send + Sync` (e.g. `Map<dyn Any + Send>`) to add those auto
/// traits.
/// - If you want the entire map to be cloneable, use `CloneAny` instead of `Any`; with
/// that, you can only add types that implement `Clone` to the map.
/// - You can add on `+ Send` or `+ Send + Sync` (e.g. `Map<dyn Any + Send>`) to add those
/// auto traits.
///
/// Cumulatively, there are thus six forms of map:
///
/// - <code>[Map]&lt;dyn [core::any::Any]&gt;</code>, also spelled [`AnyMap`] for convenience.
/// - <code>[Map]&lt;dyn [core::any::Any]&gt;</code>,
/// also spelled [`AnyMap`] for convenience.
/// - <code>[Map]&lt;dyn [core::any::Any] + Send&gt;</code>
/// - <code>[Map]&lt;dyn [core::any::Any] + Send + Sync&gt;</code>
/// - <code>[Map]&lt;dyn [CloneAny]&gt;</code>
@ -87,7 +77,7 @@ pub type RawMap<A> = HashMap<TypeId, Box<A>, BuildHasherDefault<TypeIdHasher>>;
/// <code>[anymap::Map][Map]::&lt;[core::any::Any]&gt;::new()</code> instead if desired.)
///
/// ```rust
/// let mut data = anymap::AnyMap::new();
#[doc = $example_init]
/// assert_eq!(data.get(), None::<&i32>);
/// data.insert(42i32);
/// assert_eq!(data.get(), Some(&42i32));
@ -112,7 +102,7 @@ pub struct Map<A: ?Sized + Downcast = dyn Any> {
raw: RawMap<A>,
}
// #[derive(Clone)] would want A to implement Clone, but in reality its only Box<A> that can.
// #[derive(Clone)] would want A to implement Clone, but in reality only Box<A> can.
impl<A: ?Sized + Downcast> Clone for Map<A> where Box<A>: Clone {
#[inline]
fn clone(&self) -> Map<A> {
@ -124,9 +114,9 @@ impl<A: ?Sized + Downcast> Clone for Map<A> where Box<A>: Clone {
/// The most common type of `Map`: just using `Any`; <code>[Map]&lt;dyn [Any]&gt;</code>.
///
/// Why is this a separate type alias rather than a default value for `Map<A>`? `Map::new()`
/// doesnt seem to be happy to infer that it should go with the default value.
/// Its a bit sad, really. Ah well, I guess this approach will do.
/// Why is this a separate type alias rather than a default value for `Map<A>`?
/// `Map::new()` doesnt seem to be happy to infer that it should go with the default
/// value. Its a bit sad, really. Ah well, I guess this approach will do.
pub type AnyMap = Map<dyn Any>;
impl<A: ?Sized + Downcast> Default for Map<A> {
@ -201,7 +191,8 @@ impl<A: ?Sized + Downcast> Map<A> {
self.raw.clear()
}
/// Returns a reference to the value stored in the collection for the type `T`, if it exists.
/// Returns a reference to the value stored in the collection for the type `T`,
/// if it exists.
#[inline]
pub fn get<T: IntoBox<A>>(&self) -> Option<&T> {
self.raw.get(&TypeId::of::<T>())
@ -225,7 +216,7 @@ impl<A: ?Sized + Downcast> Map<A> {
.map(|any| unsafe { *any.downcast_unchecked::<T>() })
}
// rustc 1.60.0-nightly has another method try_insert that would be nice to add when stable.
// rustc 1.60.0-nightly has another method try_insert that would be nice when stable.
/// Removes the `T` value from the collection,
/// returning it if there was one or `None` if there was not.
@ -245,11 +236,11 @@ impl<A: ?Sized + Downcast> Map<A> {
#[inline]
pub fn entry<T: IntoBox<A>>(&mut self) -> Entry<A, T> {
match self.raw.entry(TypeId::of::<T>()) {
raw_hash_map::Entry::Occupied(e) => Entry::Occupied(OccupiedEntry {
hash_map::Entry::Occupied(e) => Entry::Occupied(OccupiedEntry {
inner: e,
type_: PhantomData,
}),
raw_hash_map::Entry::Vacant(e) => Entry::Vacant(VacantEntry {
hash_map::Entry::Vacant(e) => Entry::Vacant(VacantEntry {
inner: e,
type_: PhantomData,
}),
@ -258,14 +249,8 @@ impl<A: ?Sized + Downcast> Map<A> {
/// Get access to the raw hash map that backs this.
///
/// This will seldom be useful, but its conceivable that you could wish to iterate over all
/// the items in the collection, and this lets you do that.
///
/// To improve compatibility with Cargo features, interact with this map through the names
/// [`anymap::RawMap`][RawMap] and [`anymap::raw_hash_map`][raw_hash_map], rather than through
/// `std::collections::{HashMap, hash_map}` or `hashbrown::{HashMap, hash_map}`, for anything
/// beyond self methods. Otherwise, if you use std and another crate in the tree enables
/// hashbrown, your code will break.
/// This will seldom be useful, but its conceivable that you could wish to iterate
/// over all the items in the collection, and this lets you do that.
#[inline]
pub fn as_raw(&self) -> &RawMap<A> {
&self.raw
@ -273,20 +258,14 @@ impl<A: ?Sized + Downcast> Map<A> {
/// Get mutable access to the raw hash map that backs this.
///
/// This will seldom be useful, but its conceivable that you could wish to iterate over all
/// the items in the collection mutably, or drain or something, or *possibly* even batch
/// insert, and this lets you do that.
///
/// To improve compatibility with Cargo features, interact with this map through the names
/// [`anymap::RawMap`][RawMap] and [`anymap::raw_hash_map`][raw_hash_map], rather than through
/// `std::collections::{HashMap, hash_map}` or `hashbrown::{HashMap, hash_map}`, for anything
/// beyond self methods. Otherwise, if you use std and another crate in the tree enables
/// hashbrown, your code will break.
/// This will seldom be useful, but its conceivable that you could wish to iterate
/// over all the items in the collection mutably, or drain or something, or *possibly*
/// even batch insert, and this lets you do that.
///
/// # Safety
///
/// If you insert any values to the raw map, the key (a `TypeId`) must match the values type,
/// or *undefined behaviour* will occur when you access those values.
/// If you insert any values to the raw map, the key (a `TypeId`) must match the
/// values type, or *undefined behaviour* will occur when you access those values.
///
/// (*Removing* entries is perfectly safe.)
#[inline]
@ -296,15 +275,9 @@ impl<A: ?Sized + Downcast> Map<A> {
/// Convert this into the raw hash map that backs this.
///
/// This will seldom be useful, but its conceivable that you could wish to consume all the
/// items in the collection and do *something* with some or all of them, and this lets you do
/// that, without the `unsafe` that `.as_raw_mut().drain()` would require.
///
/// To improve compatibility with Cargo features, interact with this map through the names
/// [`anymap::RawMap`][RawMap] and [`anymap::raw_hash_map`][raw_hash_map], rather than through
/// `std::collections::{HashMap, hash_map}` or `hashbrown::{HashMap, hash_map}`, for anything
/// beyond self methods. Otherwise, if you use std and another crate in the tree enables
/// hashbrown, your code will break.
/// This will seldom be useful, but its conceivable that you could wish to consume all
/// the items in the collection and do *something* with some or all of them, and this
/// lets you do that, without the `unsafe` that `.as_raw_mut().drain()` would require.
#[inline]
pub fn into_raw(self) -> RawMap<A> {
self.raw
@ -312,19 +285,14 @@ impl<A: ?Sized + Downcast> Map<A> {
/// Construct a map from a collection of raw values.
///
/// You know what? I cant immediately think of any legitimate use for this, especially because
/// of the requirement of the `BuildHasherDefault<TypeIdHasher>` generic in the map.
/// You know what? I cant immediately think of any legitimate use for this, especially
/// because of the requirement of the `BuildHasherDefault<TypeIdHasher>` generic in the
/// map.
///
/// Perhaps this will be most practical as `unsafe { Map::from_raw(iter.collect()) }`, iter
/// being an iterator over `(TypeId, Box<A>)` pairs. Eh, this method provides symmetry with
/// `into_raw`, so I dont care if literally no one ever uses it. Im not even going to write a
/// test for it, its so trivial.
///
/// To improve compatibility with Cargo features, interact with this map through the names
/// [`anymap::RawMap`][RawMap] and [`anymap::raw_hash_map`][raw_hash_map], rather than through
/// `std::collections::{HashMap, hash_map}` or `hashbrown::{HashMap, hash_map}`, for anything
/// beyond self methods. Otherwise, if you use std and another crate in the tree enables
/// hashbrown, your code will break.
/// Perhaps this will be most practical as `unsafe { Map::from_raw(iter.collect()) }`,
/// `iter` being an iterator over `(TypeId, Box<A>)` pairs. Eh, this method provides
/// symmetry with `into_raw`, so I dont care if literally no one ever uses it. Im not
/// even going to write a test for it, its so trivial.
///
/// # Safety
///
@ -347,19 +315,13 @@ impl<A: ?Sized + Downcast> Extend<Box<A>> for Map<A> {
/// A view into a single occupied location in an `Map`.
pub struct OccupiedEntry<'a, A: ?Sized + Downcast, V: 'a> {
#[cfg(all(feature = "std", not(feature = "hashbrown")))]
inner: raw_hash_map::OccupiedEntry<'a, TypeId, Box<A>>,
#[cfg(feature = "hashbrown")]
inner: raw_hash_map::OccupiedEntry<'a, TypeId, Box<A>, BuildHasherDefault<TypeIdHasher>>,
inner: hash_map::OccupiedEntry<'a, TypeId, Box<A>, $($entry_generics)?>,
type_: PhantomData<V>,
}
/// A view into a single empty location in an `Map`.
pub struct VacantEntry<'a, A: ?Sized + Downcast, V: 'a> {
#[cfg(all(feature = "std", not(feature = "hashbrown")))]
inner: raw_hash_map::VacantEntry<'a, TypeId, Box<A>>,
#[cfg(feature = "hashbrown")]
inner: raw_hash_map::VacantEntry<'a, TypeId, Box<A>, BuildHasherDefault<TypeIdHasher>>,
inner: hash_map::VacantEntry<'a, TypeId, Box<A>, $($entry_generics)?>,
type_: PhantomData<V>,
}
@ -382,8 +344,8 @@ impl<'a, A: ?Sized + Downcast, V: IntoBox<A>> Entry<'a, A, V> {
}
}
/// Ensures a value is in the entry by inserting the result of the default function if empty,
/// and returns a mutable reference to the value in the entry.
/// Ensures a value is in the entry by inserting the result of the default function if
/// empty, and returns a mutable reference to the value in the entry.
#[inline]
pub fn or_insert_with<F: FnOnce() -> V>(self, default: F) -> &'a mut V {
match self {
@ -402,8 +364,8 @@ impl<'a, A: ?Sized + Downcast, V: IntoBox<A>> Entry<'a, A, V> {
}
}
/// Provides in-place mutable access to an occupied entry before any potential inserts into the
/// map.
/// Provides in-place mutable access to an occupied entry before any potential inserts
/// into the map.
#[inline]
pub fn and_modify<F: FnOnce(&mut V)>(self, f: F) -> Self {
match self {
@ -461,37 +423,9 @@ impl<'a, A: ?Sized + Downcast, V: IntoBox<A>> VacantEntry<'a, A, V> {
}
}
/// A hasher designed to eke a little more speed out, given `TypeId`s known characteristics.
///
/// Specifically, this is a no-op hasher that expects to be fed a u64s worth of
/// randomly-distributed bits. It works well for `TypeId` (eliminating start-up time, so that my
/// get_missing benchmark is ~30ns rather than ~900ns, and being a good deal faster after that, so
/// that my insert_and_get_on_260_types benchmark is ~12μs instead of ~21.5μs), but will
/// panic in debug mode and always emit zeros in release mode for any other sorts of inputs, so
/// yeah, dont use it! 😀
#[derive(Default)]
pub struct TypeIdHasher {
value: u64,
}
impl Hasher for TypeIdHasher {
#[inline]
fn write(&mut self, bytes: &[u8]) {
// This expects to receive exactly one 64-bit value, and theres no realistic chance of
// that changing, but I dont want to depend on something that isnt expressly part of the
// contract for safety. But Im OK with release builds putting everything in one bucket
// if it *did* change (and debug builds panicking).
debug_assert_eq!(bytes.len(), 8);
let _ = bytes.try_into()
.map(|array| self.value = u64::from_ne_bytes(array));
}
#[inline]
fn finish(&self) -> u64 { self.value }
}
#[cfg(test)]
mod tests {
use crate::CloneAny;
use super::*;
#[derive(Clone, Debug, PartialEq)] struct A(i32);
@ -625,11 +559,78 @@ mod tests {
assert_debug::<Map<dyn CloneAny + Send + Sync>>();
}
#[test]
fn test_extend() {
let mut map = AnyMap::new();
// (vec![] for 1.36.0 compatibility; more recently, you should use [] instead.)
#[cfg(not(feature = "std"))]
use alloc::vec;
map.extend(vec![Box::new(123) as Box<dyn Any>, Box::new(456), Box::new(true)]);
assert_eq!(map.get(), Some(&456));
assert_eq!(map.get::<bool>(), Some(&true));
assert!(map.get::<Box<dyn Any>>().is_none());
}
}
};
}
#[cfg(feature = "std")]
everything!(
"let mut data = anymap::AnyMap::new();",
std::collections
);
#[cfg(feature = "hashbrown")]
/// AnyMap backed by `hashbrown`.
///
/// This depends on the `hashbrown` Cargo feature being enabled.
pub mod hashbrown {
use crate::TypeIdHasher;
#[cfg(doc)]
use crate::any::CloneAny;
everything!(
"let mut data = anymap::hashbrown::AnyMap::new();",
hashbrown,
BuildHasherDefault<TypeIdHasher>
);
}
/// A hasher designed to eke a little more speed out, given `TypeId`s known characteristics.
///
/// Specifically, this is a no-op hasher that expects to be fed a u64s worth of
/// randomly-distributed bits. It works well for `TypeId` (eliminating start-up time, so that my
/// get_missing benchmark is ~30ns rather than ~900ns, and being a good deal faster after that, so
/// that my insert_and_get_on_260_types benchmark is ~12μs instead of ~21.5μs), but will
/// panic in debug mode and always emit zeros in release mode for any other sorts of inputs, so
/// yeah, dont use it! 😀
#[derive(Default)]
pub struct TypeIdHasher {
value: u64,
}
impl Hasher for TypeIdHasher {
#[inline]
fn write(&mut self, bytes: &[u8]) {
// This expects to receive exactly one 64-bit value, and theres no realistic chance of
// that changing, but I dont want to depend on something that isnt expressly part of the
// contract for safety. But Im OK with release builds putting everything in one bucket
// if it *did* change (and debug builds panicking).
debug_assert_eq!(bytes.len(), 8);
let _ = bytes.try_into()
.map(|array| self.value = u64::from_ne_bytes(array));
}
#[inline]
fn finish(&self) -> u64 { self.value }
}
#[test]
fn type_id_hasher() {
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use core::hash::Hash;
use core::any::TypeId;
fn verify_hashing_with(type_id: TypeId) {
let mut hasher = TypeIdHasher::default();
type_id.hash(&mut hasher);
@ -643,13 +644,3 @@ mod tests {
verify_hashing_with(TypeId::of::<&str>());
verify_hashing_with(TypeId::of::<Vec<u8>>());
}
#[test]
fn test_extend() {
let mut map = AnyMap::new();
map.extend([Box::new(123) as Box<dyn Any>, Box::new(456), Box::new(true)]);
assert_eq!(map.get(), Some(&456));
assert_eq!(map.get::<bool>(), Some(&true));
assert!(map.get::<Box<dyn Any>>().is_none());
}
}

5
test
View file

@ -4,11 +4,10 @@ export RUSTFLAGS="-D warnings"
export RUSTDOCFLAGS="-D warnings"
run_tests() {
for release in "" "--release"; do
cargo $1 test $release --no-default-features # Not very useful without std or hashbrown, but hey, it works! (Doctests emit an error about needing a global allocator, but it exits zero anyway. ¯\_(ツ)_/¯)
cargo $1 test $release --no-default-features --features hashbrown
cargo $1 test $release --features hashbrown
# (2>/dev/null because otherwise youll keep seeing errors and double-guessing whether they were supposed to happen or whether the script failed to exit nonzero.)
! 2>/dev/null cargo $1 test $release --no-default-features || ! echo "'cargo $1 test $release --no-default-features' failed to fail (sorry, its stderr is suppressed, try it manually)"
cargo $1 test $release
cargo $1 test $release --all-features
done
}