【问题标题】:Way to replace constant item in a map using iterator使用迭代器替换地图中的常量项的方法
【发布时间】:2020-04-30 19:21:04
【问题描述】:

我有这样的代码:

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-&gt;second 指的是const Data,所以它的赋值运算符不能被调用。如果 const 被删除,它工作正常。

上面的意图是insert_or_assign,除了如果一个项目已经存在那么我需要比较新旧项目,看看哪个“更好”。

const 声明地图元素类型的目的是数据应该在地图中一次是不可变的——项目作为一个整体可以被替换,但不能被零碎地修改。

我可以通过重新分配给容器来“修复”上述

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

(其实和const有同样的问题。)

或者通过擦除并重试 emplace:

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

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

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


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

  • 如果extract 可以从容器中删除一个节点,让您修改它的 else-const key,然后重新插入它——所有这些都没有任何重新分配——为什么这对于 const mapped_value 是不可能的?
  • 可以销毁常量对象。这也应该适用于容器内的 const 对象——事实上,如果它碰巧为节点重用相同的存储(巧合或通过自定义分配器),那么擦除/替换变体可能会这样做。那么为什么没有办法replace 具有不同 const mapped_value 的 const mapped_value 而无需重新分配包含它的映射节点?

【问题讨论】:

  • "如果 const 被删除,它会正常工作。" 我想这给我们带来了一个明显的问题:当你声明时你 是认真的数据是const?因为如果你想改变一个东西,那么这意味着当你宣布所说的东西是const时你实际上并不认真。因为const 意味着你不能改变它。你自相矛盾,而 C++ 并不真正喜欢你这样做。
  • 我不希望个别成员被修改。我不介意整个对象被替换。不幸的是,C++ const 将这两个概念结合在一起。 (这确实承认另一种选择,使用非常量不可变包装器(例如std::unique_ptr&lt;Data const&gt;,指针本身是非常量的)。但这在访问时增加了令人讨厌的间接性,所以我认为它更糟。)

标签: c++ stl c++17 stdmap


【解决方案1】:

const 表示不可变,而不是部分可变。如果您在对象声明中使用const(这就是您在该模板参数中添加const 时所做的事情),C++ 相信您是认真的。它会让你坚持下去。

所以const Data 不是首发。

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

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

【讨论】:

  • 这就是我在评论中建议的,是的。但这似乎也不是首发,因为额外的间接麻烦(通过显式必须*-&gt; 间接或实现将所有方法调用委托给内部非常量实例的包装器)。不过,“在不修改原始对象的情况下替换此地图节点指向的对象”似乎是应该存在的操作。
  • @Miral:“这就是我在评论中建议的,是的。”不,您的评论建议使用 unique_ptr,这需要堆分配。我说的是一个包装器,它包含一个Data 实例,而不是指向一个实例。 "*在不修改原对象的情况下,替换这个地图节点指向的对象" 是矛盾的;您不能在不修改对象的情况下替换它。除非你摧毁它,然后进入std::launder 领域。
  • 一个在其整个生命周期中一直是 const 的对象仍然允许调用其析构函数。为什么地图不能破坏旧对象并在不违反 const 正确性的情况下构建一个新对象?
  • @Miral: That's why.
  • 我认为当我们谈论运行时容器内的某些东西时,这个论点并不适用。编译器无论如何都无法假设它包含什么值。
【解决方案2】:

在您的情况下,您真正​​想要做的是有条件地覆盖数据元素。

这强烈支持可变数据元素。

在一般情况下,容器必须具有不可变元素,因此您可以提供一个提供 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);    
}

【讨论】:

  • 这是可行的,但它需要复制大部分std::map 接口(并且通过委托,而不是using,因为类型不同)。这似乎不是一种改进。
  • @Miral 我没有太多关于您的用例的信息。我想添加您需要的必要接口大约需要 30 分钟。作为回报,您将获得改进的运行时性能。删除重新添加项目到 maep 可能有点贵。一切顺利。
  • 我尝试使用 map 的私有继承来代替;这允许using 10 种方法(map,clear,insert_or_assign,emplace,erase,empty,size,size,cbegin,swap,cend, 3(begin,end,find,隐藏它们的非常量版本),这并不太糟糕。这仍然不是完整的地图界面,但足以满足我的需求。
  • 实际上我还必须委托eraseemplaceinsert_or_assign,否则它们可能会在返回值中泄漏一个非常量迭代器。 idk,这似乎仍然应该在图书馆里。
【解决方案3】:

经过进一步的实验,我认为以下是目前适用于这种情况的最佳方法:

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

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

【讨论】:

  • 避免它的方法是遵循 Nicol 的建议,并在此映射中使数据项可变。这对于您的用例在语义上是正确的。将为说明提供答案。
猜你喜欢
  • 2021-06-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-24
  • 1970-01-01
  • 2019-03-30
  • 2021-02-25
  • 1970-01-01
相关资源
最近更新 更多