如何使用宏生成编译时唯一整数?
How to use a macro to generate compile-time unique integers?
我需要一个程序的几个部分,在不同的模块中,有一个唯一的整数。
例如:
pub fn foo() -> u64 {
unique_integer!()
}
pub fn bar() -> u64 {
unique_integer!()
}
(foo()
永远不应 return 与 bar()
相同,但值本身没有意义,不需要在构建过程中保持稳定。[=11= 的所有调用] 必须 return 相同的值,对 bar()
的所有调用也必须如此。最好,但不是必需的,值是连续的。)
有没有办法使用宏来做到这一点?
不,您不能为此使用常规宏。但是,您也许可以找到一个可以提供此功能的程序宏包。
话虽这么说...
这不算安全生锈,但是如果我们可以把安全扔掉window那么这应该可以解决问题。
macro_rules! unique_u64 {
() => {{
struct PlaceHolder;
let id = ::std::any::TypeId::of::<PlaceHolder>();
unsafe { ::std::mem::transmute::<_, u64>(id) }
}};
}
这可能是未定义的行为,但是因为我们知道每个类型都应该有一个唯一的 TypeId
,所以它会产生预期的效果。我知道这是可能的唯一原因是因为我查看了 TypeId
的结构并且知道它包含一个 u64
来区分类型。但是,目前有计划将 TypeId
从 u64
更改为更稳定且不易出现此类不安全代码的内容。我们无法保证 TypeId
的内容可能会更改为什么,当它确实更改时,如果它仍然具有与 u64
.
相同的大小,它可能会默默地失败
或者,
我们可以通过散列 TypeId
在安全生锈中获得类似的结果。现在,它稍微违反了规则,因为我们不能保证它总是会产生唯一的结果。然而,2 个不同的 TypeId
s 似乎不太可能散列为相同的值。此外,这保持在安全的 Rust 范围内,并且不太可能在未来的 Rust 版本中被破坏。
macro_rules! unique_u64 {
() => {{
use ::std::hash::{Hash, Hasher};
struct PlaceHolder;
let id = ::std::any::TypeId::of::<PlaceHolder>();
let mut hasher = ::std::collections::hash_map::DefaultHasher::new();
id.hash(&mut hasher);
hasher.finish()
}};
}
可以用 once_cell
做这样的事情,使用静态原子变量作为计数器:
use core::sync::atomic::{Ordering, AtomicU64};
use once_cell::sync::Lazy;
static COUNTER: AtomicU64 = AtomicU64::new(0);
fn foo() -> u64 {
static LOCAL_COUNTER: Lazy<u64> = Lazy::new(|| COUNTER.fetch_add(1, Ordering::Relaxed));
*LOCAL_COUNTER
}
fn bar() -> u64 {
static LOCAL_COUNTER: Lazy<u64> = Lazy::new(|| COUNTER.fetch_add(1, Ordering::Relaxed));
*LOCAL_COUNTER
}
fn main() {
dbg!(foo()); // 0
dbg!(foo()); // still 0
dbg!(bar()); // 1
dbg!(foo()); // unchanged - 0
dbg!(bar()); // unchanged - 1
}
而且,是的,重复代码可以像往常一样用宏包装:
macro_rules! unique_integer {
() => {{
static LOCAL_COUNTER: Lazy<u64> = Lazy::new(|| COUNTER.fetch_add(1, Ordering::Relaxed));
*LOCAL_COUNTER
}}
}
fn foo() -> u64 {
unique_integer!()
}
fn bar() -> u64 {
unique_integer!()
}
不完全是一个宏,但无论如何它是一个命题:
#[repr(u64)]
enum Unique {
Foo,
Bar,
}
pub fn foo() -> u64 {
Unique::Foo as u64
}
pub fn bar() -> u64 {
Unique::Bar as u64
}
如果您不使用变体,编译器应该会警告您。
您可以使用模块路径(包含通向文件的包和模块)、文件名、列和宏调用的行号来计算 compile-time 散列,如下所示:
pub const fn hash(module_path: &'static str, file: &'static str, line: u32, column: u32) -> u64 {
let mut hash = 0xcbf29ce484222325;
let prime = 0x00000100000001B3;
let mut bytes = module_path.as_bytes();
let mut i = 0;
while i < bytes.len() {
hash ^= bytes[i] as u64;
hash = hash.wrapping_mul(prime);
i += 1;
}
bytes = file.as_bytes();
i = 0;
while i < bytes.len() {
hash ^= bytes[i] as u64;
hash = hash.wrapping_mul(prime);
i += 1;
}
hash ^= line as u64;
hash = hash.wrapping_mul(prime);
hash ^= column as u64;
hash = hash.wrapping_mul(prime);
hash
}
macro_rules! unique_number {
() => {{
const UNIQ: u64 = crate::hash(module_path!(), file!(), line!(), column!());
UNIQ
}};
}
fn foo() -> u64 {
unique_number!()
}
fn bar() -> u64 {
unique_number!()
}
fn main() {
println!("{} {}", foo(), bar()); // 2098219922142993841 2094402417770602149 on the playground
}
与可以根据调用顺序 return 不同值的最佳答案相比,这具有一致结果的好处,并且这也完全在编译时计算,从而消除了运行时开销维护一个柜台。
唯一的缺点是您可能会遇到散列值冲突。但机会很小。如果需要,您可以尝试实现一种计算完美哈希值的算法。示例中使用了FNV算法,应该不错,但并不完美。
我需要一个程序的几个部分,在不同的模块中,有一个唯一的整数。
例如:
pub fn foo() -> u64 {
unique_integer!()
}
pub fn bar() -> u64 {
unique_integer!()
}
(foo()
永远不应 return 与 bar()
相同,但值本身没有意义,不需要在构建过程中保持稳定。[=11= 的所有调用] 必须 return 相同的值,对 bar()
的所有调用也必须如此。最好,但不是必需的,值是连续的。)
有没有办法使用宏来做到这一点?
不,您不能为此使用常规宏。但是,您也许可以找到一个可以提供此功能的程序宏包。
话虽这么说...
这不算安全生锈,但是如果我们可以把安全扔掉window那么这应该可以解决问题。
macro_rules! unique_u64 {
() => {{
struct PlaceHolder;
let id = ::std::any::TypeId::of::<PlaceHolder>();
unsafe { ::std::mem::transmute::<_, u64>(id) }
}};
}
这可能是未定义的行为,但是因为我们知道每个类型都应该有一个唯一的 TypeId
,所以它会产生预期的效果。我知道这是可能的唯一原因是因为我查看了 TypeId
的结构并且知道它包含一个 u64
来区分类型。但是,目前有计划将 TypeId
从 u64
更改为更稳定且不易出现此类不安全代码的内容。我们无法保证 TypeId
的内容可能会更改为什么,当它确实更改时,如果它仍然具有与 u64
.
或者,
我们可以通过散列 TypeId
在安全生锈中获得类似的结果。现在,它稍微违反了规则,因为我们不能保证它总是会产生唯一的结果。然而,2 个不同的 TypeId
s 似乎不太可能散列为相同的值。此外,这保持在安全的 Rust 范围内,并且不太可能在未来的 Rust 版本中被破坏。
macro_rules! unique_u64 {
() => {{
use ::std::hash::{Hash, Hasher};
struct PlaceHolder;
let id = ::std::any::TypeId::of::<PlaceHolder>();
let mut hasher = ::std::collections::hash_map::DefaultHasher::new();
id.hash(&mut hasher);
hasher.finish()
}};
}
可以用 once_cell
做这样的事情,使用静态原子变量作为计数器:
use core::sync::atomic::{Ordering, AtomicU64};
use once_cell::sync::Lazy;
static COUNTER: AtomicU64 = AtomicU64::new(0);
fn foo() -> u64 {
static LOCAL_COUNTER: Lazy<u64> = Lazy::new(|| COUNTER.fetch_add(1, Ordering::Relaxed));
*LOCAL_COUNTER
}
fn bar() -> u64 {
static LOCAL_COUNTER: Lazy<u64> = Lazy::new(|| COUNTER.fetch_add(1, Ordering::Relaxed));
*LOCAL_COUNTER
}
fn main() {
dbg!(foo()); // 0
dbg!(foo()); // still 0
dbg!(bar()); // 1
dbg!(foo()); // unchanged - 0
dbg!(bar()); // unchanged - 1
}
而且,是的,重复代码可以像往常一样用宏包装:
macro_rules! unique_integer {
() => {{
static LOCAL_COUNTER: Lazy<u64> = Lazy::new(|| COUNTER.fetch_add(1, Ordering::Relaxed));
*LOCAL_COUNTER
}}
}
fn foo() -> u64 {
unique_integer!()
}
fn bar() -> u64 {
unique_integer!()
}
不完全是一个宏,但无论如何它是一个命题:
#[repr(u64)]
enum Unique {
Foo,
Bar,
}
pub fn foo() -> u64 {
Unique::Foo as u64
}
pub fn bar() -> u64 {
Unique::Bar as u64
}
如果您不使用变体,编译器应该会警告您。
您可以使用模块路径(包含通向文件的包和模块)、文件名、列和宏调用的行号来计算 compile-time 散列,如下所示:
pub const fn hash(module_path: &'static str, file: &'static str, line: u32, column: u32) -> u64 {
let mut hash = 0xcbf29ce484222325;
let prime = 0x00000100000001B3;
let mut bytes = module_path.as_bytes();
let mut i = 0;
while i < bytes.len() {
hash ^= bytes[i] as u64;
hash = hash.wrapping_mul(prime);
i += 1;
}
bytes = file.as_bytes();
i = 0;
while i < bytes.len() {
hash ^= bytes[i] as u64;
hash = hash.wrapping_mul(prime);
i += 1;
}
hash ^= line as u64;
hash = hash.wrapping_mul(prime);
hash ^= column as u64;
hash = hash.wrapping_mul(prime);
hash
}
macro_rules! unique_number {
() => {{
const UNIQ: u64 = crate::hash(module_path!(), file!(), line!(), column!());
UNIQ
}};
}
fn foo() -> u64 {
unique_number!()
}
fn bar() -> u64 {
unique_number!()
}
fn main() {
println!("{} {}", foo(), bar()); // 2098219922142993841 2094402417770602149 on the playground
}
与可以根据调用顺序 return 不同值的最佳答案相比,这具有一致结果的好处,并且这也完全在编译时计算,从而消除了运行时开销维护一个柜台。
唯一的缺点是您可能会遇到散列值冲突。但机会很小。如果需要,您可以尝试实现一种计算完美哈希值的算法。示例中使用了FNV算法,应该不错,但并不完美。