我有一个存储tracker.setScreenName("testscreen")
tracker.send(HitBuilders.ScreenViewBuilder().build())
的东西的类。在我的程序中,我为此类的对象创建了std::vector
的{{1}}(请参见下面的代码)。我定义了自定义函数来计算哈希值和相等性,以便std::unordered_set
与对象(而不是指针)一起工作。这意味着:指向具有相同内容的不同对象的两个不同指针应被视为相等,我们称其为“等效”。 >
到目前为止,一切都按预期工作,但现在我偶然发现了一个奇怪的行为:我向std::shared_ptr
添加了指向对象的指针,并为内容相同的不同对象创建了不同的指针。如前所述,我期望unordered_set
将向存储在集合中的等效指针返回有效的迭代器。但事实并非如此。
这是一个最小的工作代码示例。
unordered_set
输出
my_set.find(different_object)
这是问题所在:
#include <boost/functional/hash.hpp>
#include <cstdlib>
#include <functional>
#include <iostream>
#include <memory>
#include <unordered_set>
#include <vector>
class Foo {
public:
Foo() {}
bool operator==(Foo const & rhs) const {
return bar == rhs.bar;
}
std::vector<int> bar;
};
struct FooHash {
size_t operator()(std::shared_ptr<Foo> const & foo) const {
size_t seed = 0;
for (size_t i = 0; i < foo->bar.size(); ++i) {
boost::hash_combine(seed, foo->bar[i]);
}
return seed;
}
};
struct FooEq {
bool operator()(std::shared_ptr<Foo> const & rhs,
std::shared_ptr<Foo> const & lhs) const {
return *lhs == *rhs;
}
};
int main() {
std::unordered_set<std::shared_ptr<Foo>, FooHash, FooEq> fooSet;
auto empl = fooSet.emplace(std::make_shared<Foo>());
(*(empl.first))->bar.emplace_back(0);
auto baz = std::make_shared<Foo>();
baz->bar.emplace_back(0);
auto eqFun = fooSet.key_eq();
auto hashFun = fooSet.hash_function();
if (**fooSet.begin() == *baz) {
std::cout << "Objects equal" << std::endl;
}
if (eqFun(*fooSet.begin(), baz)) {
std::cout << "Keys equal" << std::endl;
}
if (hashFun(*fooSet.begin()) == hashFun(baz)) {
std::cout << "Hashes equal" << std::endl;
}
if (fooSet.find(baz) != fooSet.end()) {
std::cout << "Baz in fooSet" << std::endl;
} else {
std::cout << "Baz not in fooSet" << std::endl;
}
return 0;
}
我在这里想念什么?为什么集合找不到等效对象?
可能感兴趣:我对此进行了试验,发现如果我的班级存储的是普通的Objects equal
Keys equal
Hashes equal
而不是Baz not in fooSet
,那么它可以工作。如果我坚持使用int
,但将其构造函数更改为
std::vector
并使用
初始化我的对象std::vector
它也可以工作。如果删除整个指针内容,它将中断Foo(int i) : bar{i} {}
并返回常量迭代器,因此无法修改集合中的对象(以这种方式)。但是,无论如何,这些更改均不适用于我的真实程序。
我使用std::make_shared<Foo>(0);
的{{1}}版本7.3.0
答案 0 :(得分:8)
您不能修改集合的元素(并希望集合起作用)。因为您提供了FooHash
和FooEq
来检查参照对象的值,所以从集合的角度来看,这使参照对象成为值的一部分!
如果我们更改fooSet
的初始化以在插入元素 之前进行设置,则会得到您想要/期望的结果:
std::unordered_set<std::shared_ptr<Foo>, FooHash, FooEq> fooSet;
auto e = std::make_shared<Foo>();
e->bar.emplace_back(0); // modification is _before_
fooSet.insert(e); // insertion
在集合中查找对象取决于哈希值不变。如果在添加成员后确实需要修改成员,则需要将其删除,进行更改,然后添加修改后的对象-请参见Yakk's answer。
为避免遇到此类问题,将std::shared_ptr<const Foo>
用作元素可能更安全,这将防止通过集合修改指向Foo
的位置(尽管您仍然要对使用您可能还拥有的任何非const指针。
答案 1 :(得分:3)
任何使==
中元素的哈希或unordered_set
结果违反unordered_set
规则的操作都是错误的;结果是未定义的行为。
您更改了unordered_set
中某个元素的哈希结果,因为您的元素是共享指针,但是它们的哈希和==
基于所指向的值。然后您的代码将更改所指向的值。
使用代码std::shared_ptr<Foo>
中的所有std::shared_ptr<Foo const>
。
这包括等号和哈希码以及无序设置码。
auto empl = fooSet.emplace(std::make_shared<Foo>());
(*(empl.first))->bar.emplace_back(0);
此代码是正确的,并且安全后(之后)将无法编译。
如果您要更改fooSet
中的元素,
template<class C, class It, class F>
void mutate(C& c, It it, F&& f) {
auto e = *it->first;
f(e); // do this before erasing, more exception-safe
auto new_elem = std::make_shared<decltype(e)>(std::move(e));
c.erase(it);
c.insert( new_elem ); // could throw, but hard to avoid.
}
现在代码显示为:
auto empl = fooSet.emplace(std::make_shared<Foo>());
mutate(fooSet, empl.first, [&](auto&& elem) {
elem.emplace_back(0);
});
mutate
复制出一个元素,从集合中删除指针,调用该元素上的函数,然后将其重新插入fooSet
中。
在这种情况下,当然是愚蠢的;我们只是把它放进去了,现在我们把它取出变异,然后放回去。
但是在更一般的情况下,它不会那么笨。
答案 2 :(得分:0)
在此处添加一个对象,并使用其当前的哈希值对其进行存储。
auto empl = fooSet.emplace(std::make_shared<Foo>());
您在此处更改哈希值:
(*(empl.first))->bar.emplace_back(0);
现在,该集合具有使用旧/错误哈希值存储的对象。如果需要更改对象中影响其哈希值的任何内容,则需要提取该对象,对其进行更改并重新插入。如果使用该类的所有可变成员来计算哈希值,请改为将其设置为<const Foo>
的集合。
为了使以后shared_ptr<const Foo>
集的声明更加容易,您还可以使用您的专业化扩展std名称空间。
class Foo {
public:
Foo() {}
size_t hash() const {
size_t seed = 0;
for (auto& b : bar) {
boost::hash_combine(seed, b);
}
return seed;
}
bool operator==(Foo const & rhs) const {
return bar == rhs.bar;
}
std::vector<int> bar;
};
namespace std {
template<>
struct hash<Foo> {
size_t operator()(const Foo& foo) const {
return foo.hash();
}
};
template<>
struct hash<std::shared_ptr<const Foo>> {
size_t operator()(const std::shared_ptr<const Foo>& foo) const {
/* A version using std::hash<Foo>:
std::hash<Foo> hasher;
return hasher(*foo);
*/
return foo->hash();
}
};
template<>
struct equal_to<std::shared_ptr<const Foo>> {
bool operator()(std::shared_ptr<const Foo> const & rhs,
std::shared_ptr<const Foo> const & lhs) const {
return *lhs == *rhs;
}
};
}
有了这个,您可以像这样简单地声明您的unordered_set:
std::unordered_set<std::shared_ptr<const Foo>> fooSet;
现在与这样声明一样:
std::unordered_set<
std::shared_ptr<const Foo>,
std::hash<std::shared_ptr<const Foo>>,
std::equal_to<std::shared_ptr<const Foo>>
> fooSet;