shared_ptr的unordered_set找不到它已存储的等效对象

时间:2018-10-15 16:28:18

标签: c++ c++17

我有一个存储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

进行编译

3 个答案:

答案 0 :(得分:8)

您不能修改集合的元素(并希望集合起作用)。因为您提供了FooHashFooEq来检查参照对象的值,所以从集合的角度来看,这使参照对象成为值的一部分

如果我们更改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;