请考虑以下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
来修改z
的成员MyStruct
。我想知道这是否合法和合法,因为该集合是a)无序和b)我没有使用z
进行散列或相等?
我会说这是“Mutable”关键字的完美用法。
mutable关键字用于标记不属于类的“状态”的成员(即它们是某种形式的缓存或中间值,不表示对象的逻辑状态)。
您的相等运算符(以及其他比较器(或任何序列化数据的函数)(或生成哈希的函数))定义对象的状态。你的相等比较器在检查对象的逻辑状态时不使用成员'z',因此成员'z'不是类的状态的一部分,因此使用“可变”访问器是不可行的。
现在这样说。我认为这样写代码非常脆弱。类中没有任何东西阻止未来的维护者意外地使z成为类的状态(即将其添加到散列函数),从而打破了在std::unordered_set<>
中使用它的前提条件。因此,您应该非常明智地使用它并花费大量时间编写注释和单元测试以确保维护前提条件。
我还会研究“@Andriy Tylychko”关于将类分为const部分和值部分的评论,以便您可以将它作为std::unordered_map
的一部分使用。
问题是z
不仅仅是在特定类型的unordered_set
环境中的对象状态的一部分。如果一个人继续这条路线,那么为了以防万一,最终会让一切变得可变。
通常,您所要求的是不可能的,因为元素哈希需要在修改元素时自动重新计算。
你可以做的最普遍的事情是有一个元素修改协议,类似于Boost.MultiIndex modify
中的https://www.boost.org/doc/libs/1_68_0/libs/multi_index/doc/reference/ord_indices.html#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;
}
典型的使用的可变性是允许const方法改变不构成对象的基本状态的一部分的数据成员,例如,从对象的非可变数据派生的延迟评估值。声明公共数据成员是可变的并不是一种好的做法,即使该对象被标记为const,您也允许对象状态在外部进行更改。
在您的示例代码中,您使用了mutable,因为(根据您的注释),如果没有它,您的代码将无法编译。您的代码无法编译,因为从emplace返回的迭代器是const。
有两种不正确的方法可以解决这个问题,一种是使用mutable关键字,另一种是几乎同样糟糕的方法是将const引用转换为非const引用。
emplace方法旨在将对象直接构造到集合中,并避免复制构造函数调用。这是一个有用的优化,但如果它会损害代码的可维护性,则不应使用它。您应该在构造函数中初始化z,或者不应该使用emplace将对象添加到集合中,而是设置z的值,然后将对象插入到集合中。
如果你的对象在构造之后永远不需要改变,你应该通过声明const或者声明数据成员是私有的并且添加非变异访问器方法(这些应该被声明为const)来使你的类/结构不可变。