使用 mutable 允许修改 unordered_set 中的对象

Using mutable to allow modification of object in unordered_set

请考虑以下内容code

#include <iostream>
#include <unordered_set>

struct MyStruct
{
    int x, y;
    double mutable z;

    MyStruct(int x, int y)
        : x{ x }, y{ y }, z{ 0.0 }
    {
    }
};

struct MyStructHash
{
    inline size_t operator()(MyStruct const &s) const
    {
        size_t ret = s.x;
        ret *= 2654435761U;
        return ret ^ s.y;
    }
};

struct MyStructEqual
{
    inline bool operator()(MyStruct const &s1, MyStruct const &s2) const
    {
        return s1.x == s2.x && s1.y == s2.y;
    }
};

int main()
{
    std::unordered_set<MyStruct, MyStructHash, MyStructEqual> set;
    auto pair = set.emplace(100, 200);

    if (pair.second)
        pair.first->z = 300.0;

    std::cout << set.begin()->z;
}

我正在使用 mutable 允许修改 MyStruct 的成员 z。我想知道这是否可行且合法,因为该集合是 a) 无序的和 b) 我没有使用 z 进行散列或相等?

典型使用的可变变量是允许 const 方法更改不构成对象基本状态一部分的数据成员,例如从对象的非可变数据派生的惰性评估值。 声明 public 数据成员可变不是一个好习惯,即使该对象被标记为 const,您也允许在外部更改对象状态。

在您的示例代码中,您使用了 mutable,因为(根据您的评论),没有它您的代码将无法编译。您的代码无法编译,因为从 emplace 返回的迭代器是 const.

有两种不正确的方法来解决这个问题,一种是使用 mutable 关键字,另一种几乎同样糟糕的方法是将 const 引用强制转换为非常量引用。

emplace 方法旨在将对象直接构建到集合中,避免调用复制构造函数。这是一个有用的优化,但如果它会损害代码的可维护性,则不应使用它。您应该在构造函数中初始化 z,或者您不应该使用 emplace 将对象添加到集合中,而是设置 z 的值,然后将对象插入到集合中。

如果您的对象在构造后永远不需要更改,您应该通过将它们声明为 const 或将数据成员声明为私有并添加非可变访问器方法(这些应该声明为 const)来使您的 class/struct 不可变.

我想说这是对 "Mutable" 关键字的完美使用。

可变关键字用于标记不属于 class 的 "state" 的成员(即它们是某种形式的缓存或中间值,不代表逻辑状态对象)。

您的相等运算符(以及其他比较器(或序列化数据的任何函数)(或生成哈希的函数))定义对象的状态。您的相等比较器在检查对象的逻辑状态时不使用成员 'z',因此成员 'z' 不是 class 状态的一部分,因此使用"mutable" 存取器。

现在说。我确实认为以这种方式编写代码非常脆弱。 class 中没有任何内容可以阻止未来的维护者不小心使 z 成为 class 状态的一部分(即将其添加到哈希函数),从而打破在 std::unordered_set<>。因此,您在使用它时应该非常明智,并花大量时间编写注释和单元测试以确保维持先决条件。

我还会研究“@Andriy Tylychko”关于将 class 分解为常量部分和值部分的评论,以便您可以将其用作 std::unordered_map 的一部分。

问题是 z 不是对象状态的一部分 在特定类型 unordered_set 的上下文中。 如果继续这条路线,为了以防万一,最终会让一切都变得可变。

一般来说,你问的不是 possible,因为元素哈希需要在元素修改时自动重新计算。

most 一般你可以做的是有一个元素修改的协议,类似于 Boost.MultiIndex https://www.boost.org/doc/libs/1_68_0/libs/multi_index/doc/reference/ord_indices.html#modify 中的 modify 功能。 代码很丑陋,但由于 extract 的存在,它可以在重要时变得相当高效(好吧,你的特定结构仍然不会从移动中受益)。

template<class UnorderedSet, class It, class F>
void modify(UnorderedSet& s, It it, F f){
    It h = it; ++h;
    auto val = std::move(s.extract(it).value());
    f(val);
    s.emplace_hint(h, std::move(val) );
}

int main(){
    std::unordered_set<MyStruct, MyStructHash, MyStructEqual> set;
    auto pair = set.emplace(100, 200);

    if (pair.second) modify(set, pair.first, [](auto&& e){e.z = 300;});

    std::cout << set.begin()->z;
}

(代码未经测试)


@JoaquinMLopezMuños(Boost.MultiIndex 的作者)建议重新插入整个节点。我认为这会像这样工作:

template<class UnorderedSet, class It, class F>
void modify(UnorderedSet& s, It it, F f){
    It h = it; ++h;
    auto node = s.extract(it);
    f(node.value());
    s.insert(h, std::move(node));
}

EDIT2:最终测试代码,需要 C++17(用于提取)

#include <iostream>
#include <unordered_set>

struct MyStruct
{
    int x, y;
    double  z;

    MyStruct(int x, int y)
        : x{ x }, y{ y }, z{ 0.0 }
    {
    }

};

struct MyStructHash
{
    inline size_t operator()(MyStruct const &s) const
    {
        size_t ret = s.x;
        ret *= 2654435761U;
        return ret ^ s.y;
    }
};

struct MyStructEqual
{
    inline bool operator()(MyStruct const &s1, MyStruct const &s2) const
    {
        return s1.x == s2.x && s1.y == s2.y;
    }
};

template<class UnorderedSet, class It, class F>
void modify(UnorderedSet& s, It it, F f){
    auto node = s.extract(it++);
    f(node.value());
    s.insert(it, std::move(node));
}

int main(){
    std::unordered_set<MyStruct, MyStructHash, MyStructEqual> set;
    auto pair = set.emplace(100, 200);

    if(pair.second) modify(set, pair.first, [](auto&& e){e.z = 300;});

    std::cout << set.begin()->z;
}