使用迭代器替换映射中的常量项的方法

Way to replace constant item in a map using iterator

本文关键字:常量 方法 映射 迭代器 替换      更新时间:2023-10-16

我有这样的代码:

std::map<int, const Data> all_data;
// ...
bool OldDataIsBetter(Data const& oldData, Data const& newData) {...}
// ...
void AddData(int key, Data&& newData)
{
auto [it, ok] = all_data.try_emplace(key, std::move(newData));
if (!ok)
{
if (OldDataIsBetter(*it, newData)) return;
it->second = std::move(newData);
}
}

这不会编译,因为it->second引用const Data,因此无法调用其赋值运算符。 如果删除const它工作正常。

上述的目的是insert_or_assign,除了如果一个项目已经存在,那么我需要比较新旧项目以查看哪个"更好"。

使用const声明 map 元素类型的意图是,数据在映射中一次应该是不可变的 - 整个项目可以替换,但不能逐个修改。

我可以通过重新分配给容器来"修复"上述问题:

all_data[key] = std::move(newData);

(实际上事实证明,const也有同样的问题。

或者通过擦除并重试放置:

all_data.erase(it);
all_data.emplace(key, std::move(newData)); // should never fail

但是这些看起来都不优雅,因为我已经有一个迭代器指向应该替换的项目,而上面的两个都忘记了这一点并再次搜索。

有没有更好的方法来完成这种替换?


来自聊天线程的TLDR提出了相关问题:

  • 如果extract可以从容器中删除节点,让您修改其其他常量键,然后重新插入它 - 所有这些都没有任何重新分配 - 为什么对于常量mapped_value来说这是不可能的?
  • 可以销毁常量对象。 这也应该适用于容器内的 const 对象——事实上,如果 erase/emplace 变体碰巧为节点重用相同的存储(巧合或通过自定义分配器(,这就是它可能会做的事情。 那么为什么没有办法在不重新分配包含它的映射节点的情况下,使用不同的常量mapped_valuereplace常量mapped_value呢?

const表示不可变,而不是部分可变。如果您在对象声明上使用const(当您将const粘贴到该模板参数中时,这就是您正在执行的操作(,C++相信您是认真的。它会让你坚持下去。

所以const Data是不可能实现的。

从你的问题中,我推测Data有一些函数来设置其状态的某些部分,并且你不希望用户调用所述函数。但您希望用户能够覆盖该值。

执行此操作的方法是提供一个包装Data实例的对象类型,允许从Data对象进行赋值。此包装器将提供Data提供的const访问器的版本。但是该类型不提供非const修饰符Data

在您的情况下,您真正希望能够做的是有条件地覆盖数据元素。

这强烈主张可变数据元素。

在一般情况下,容器必须具有不可变元素,因此您可以提供一个包装器来提供 const 访问,除非特别请求可变访问:

#include <map>
#include <functional>
struct Data {};
struct MyDataMap
{
using store_type = std::map<int, Data>;
using iterator = store_type::const_iterator;
template<class Condition>
std::pair<iterator, bool> 
replace_if(int key, Data&& value, Condition cond)
{
auto [it, inserted_or_replaced] = store_.try_emplace(key, std::move(value));
if (!inserted_or_replaced && cond(it->second, value))
{
it->second = std::move(value);
inserted_or_replaced = true;
}
return std::make_pair(it, inserted_or_replaced);
}
// other accessors as necessary
iterator begin() const { return store_.cbegin(); }
iterator end() const { return store_.cend(); }
private:
store_type store_;
};
// ...
bool OldDataIsBetter(Data const& oldData, Data const& newData);
// ...
void test(MyDataMap& m, int k, Data newd)
{
auto oldIsWorse = std::not_fn(OldDataIsBetter);
auto [it, replaced] = m.replace_if(k, std::move(newd), oldIsWorse);    
}

经过一些进一步的实验,我认为以下是目前可用于此场景的最佳方法:

it = all_data.erase(it);
all_data.emplace_hint(it, key, std::move(newData)); // should never fail

这对我来说似乎仍然不理想,因为它仍然会重新分配地图节点。 这不是世界末日,但如果可能的话,我想找到一种方法来避免它。