对于BTreeMap和其他依赖于Ord的东西,是否有等效于C++比较器对象?

Is there an equivalent to C++ comparator objects for BTreeMap and other things that rely on Ord?

本文关键字:C++ 对象 比较器 是否 其他 BTreeMap 依赖于 Ord 对于      更新时间:2023-10-16

我似乎无法用Ord特征完成与C++比较器对象相同的事情。以这种对更复杂场景的简化为例:

int epsilon = 0.001;
auto less = [epsilon](const double a, const double b) {
if (fabs(a - b) > epsilon) {
return a < b;
}
else {
return false;
}
};
std::map<float, int, decltype(less)> myMap(less);
myMap.insert(std::make_pair(1.0, 1));
myMap.insert(std::make_pair(2.0, 2));
myMap.insert(std::make_pair(3.0, 3));
auto found = myMap.find(1.0001);
assert(found != myMap.end());
assert(found->second == 1);

我希望比较器具有一些运行时上下文,例如 epsilon 值。我不知道您将如何使用BTreeMap做到这一点,因为我无法将状态添加到Ord特征中。有没有一些我还没有想出的技巧来做等效的?

编辑

我的例子中有一个错别字,使我无法意识到C++实际上不起作用。我正在重新评估这个问题。

浮点类型不实现Ord是有原因的。BTreeMap的内部对Ord的实现者做出假设,这使得它更有效率,但如果这些假设被证明是不正确的,那么它可能导致未定义的行为,因为这些假设在unsafe代码中被依赖。浮点不能满足这些假设,因为存在NaN,表示无穷大的值和浮点运算的性质,这意味着"相同"的数字可以有不同的二进制表示。

您的C++代码可能会遇到相同的问题,但有时具有未定义行为的代码似乎可以正常工作。直到有一天它没有 - 这就是未定义行为的本质!

浮点数适用于表示度量值或值可以具有大量变化的数量级的位置。如果你计算城市之间的距离,数字相差几纳米,你不在乎。你永远不必找到另一个与伦敦到纽约的距离完全相同的城市。更有可能的是,您会很乐意搜索与最近的 1 公里距离相同的城市 - 您可以将其作为整数进行比较。

这让我想到了一个问题,你为什么要使用浮点数作为键?这对您意味着什么,您想在那里存储什么?f64::NAN是您希望有效的值吗?45.0000000000145.00000000001001"一样"吗?您是否存储非常大的数字和非常小的数字一样可能?对于这些数字来说,精确相等有意义吗?它们是否是计算的结果,可能会产生舍入误差?

不可能告诉你在这里该做什么,但我可以建议你看看你的真正问题,并以反映你真正约束的方式对它进行建模。如果您只关心特定的精度,则将数字舍入到该精度并将它们存储在定点类型、整数或有理数中,所有这些都实现了Ord

根据您的C++代码,看起来您对0.001的精度感到满意。因此,您可以将密钥存储为整数 - 您只需要记住进行转换并根据需要乘以/除以 1000。你将不得不处理NaN和无穷大的可能性,但你会在安全的代码中做到这一点,所以你不必担心 UB。

以下是如何使用num-rational箱来获取与C++代码具有类似行为的东西:

extern crate num_rational; // 0.2.1
use num_rational::Rational64;
use std::collections::BTreeMap;
fn in_thousandths(n: f64) -> Rational64 {
// you may want to include further validation here to deal with `NaN` or infinities
let denom = 1000;
Rational64::new_raw((n * denom as f64) as i64, denom)
}
fn main() {
let mut map = BTreeMap::new();
map.insert(in_thousandths(1.0), 1);
map.insert(in_thousandths(2.0), 2);
map.insert(in_thousandths(3.0), 3);
let value = map.get(&1.into());
assert_eq!(Some(&1), value);
}

我认为你应该使用自己的类型并实现自己的 Ord 特征:

#[derive(PartialOrd, Debug)]
struct MyNumber {
value: f64,
}
impl MyNumber {
const EPSILON: f64 = 0.001;
fn new(value: f64) -> Self {
Self { value }
}
}
impl PartialEq for MyNumber {
fn eq(&self, other: &MyNumber) -> bool {
if f64::abs(self.value - other.value) < MyNumber::EPSILON {
return true;
} else {
return false;
}
}
}
impl Eq for MyNumber {}
use std::cmp::Ordering;
impl Ord for MyNumber {
fn cmp(&self, other: &MyNumber) -> Ordering {
if self == other {
Ordering::Equal
} else if self < other {
Ordering::Less
} else {
Ordering::Greater
}
}
}
fn main() {
use std::collections::BTreeMap;
let mut map = BTreeMap::new();
map.insert(MyNumber::new(1.0), 1);
map.insert(MyNumber::new(2.0), 2);
map.insert(MyNumber::new(3.0), 3);
println!("{:?}", map.get(&MyNumber::new(1.0001)));
}

但我认为它不符合BTreeMap的要求。

在 rust 中做到这一点的方法是将你的密钥包装在 newtype 中,并按照你想要的方式实现Ord

use std::cmp::Ordering;
use std::collections::BTreeMap;
const EPSILON: f64 = 0.001;
struct MyFloat (f64);
impl Ord for MyFloat {
fn cmp (&self, other: &MyFloat) -> Ordering {
if (self.0 - other.0).abs() < EPSILON {
// I assume that this is what you wanted even though the C++ example
// does the opposite
Ordering::Equal
} else if self.0 < other.0 {
Ordering::Less
} else {
Ordering::Greater
}
}
}
impl PartialOrd for MyFloat {
fn partial_cmp (&self, other: &MyFloat) -> Option<Ordering> {
Some (self.cmp (other))
}
}
impl PartialEq for MyFloat {
fn eq (&self, other: &MyFloat) -> bool {
(self.0 - other.0).abs() < EPSILON
}
}
impl Eq for MyFloat {}
fn main() {
let mut map = BTreeMap::new();
map.insert (MyFloat (1.0), 1);
map.insert (MyFloat (2.0), 2);
map.insert (MyFloat (3.0), 3);
let found = map.get (&MyFloat (1.00001));
println!("Found: {:?}", found);
}

操场

请注意,仅当键有限且间隔超过EPSILON时,这才有效。