C++中以自定义类为std::map键时如何修改键对象非key属性
我有如下类定义:
#include <string> class Item { public: Item(std::string name, int id); virtual int getWeight() = 0; protected: std::string name; const int id; };
#include <vector> #include <memory> #include "Item.h" class Bucket : Item { public: Bucket(std::string name, std::string type, int id); // 实现中会调用Item的构造函数 bool operator<(const Bucket& b) const { return (id < b.id ); } void addItem(std::shared_ptr<Item> item) { this->weight += item->getWeight(); this->items.push_back(item); } int getWeight() override; // 该实现与当前问题无关 private: std::string type; std::vector<std::shared_ptr<Item>> items; int weight = 0; };
还有其他类继承自Item类,为简化问题此处仅展示Bucket类。
现在我要在main函数中遍历已经存在条目的map,调用方法修改键对象的某个属性,main.cpp代码如下:
#include <map> #include <memory> #include <vector> #include <algorithm> #include "Item.h" #include "Bucket.h" using namespace std; int main(){ map<Bucket, vector<shared_ptr<Item>>> map; // 假设该map已有若干条目 for(auto itr = map.begin(); itr != map.end(); itr++){ for_each(itr->second.begin(), itr->second.end(), [&itr](shared_ptr<Item> item){ itr->first.addItem(item); // 此处编译不通过,错误信息见下文 }); } }
如上代码在itr->first.addItem(item);行编译报错:'this' argument to member function 'addItem' has type 'const Bucket', but function is not marked const.
我不能将addItem方法标记为const,因为它会修改对象属性,修改为const也会报错。我理解的问题原因是Bucket作为map的键存入后会变为const类型。请问有没有办法不将map改为map<int, pair<Bucket,vector<shared_ptr<Item>>>>的结构,仅告知编译器只将Bucket的id属性作为map的const键,从而可以修改Bucket的其他属性?还是我对问题根源的理解有误?
你对问题根源的理解完全正确:std::map作为有序关联容器,内部依赖键的排序结果维护红黑树结构,因此所有键默认是const只读的,禁止修改避免破坏容器结构。
你的Bucket类的比较运算符仅用到了本身就是const修饰的id字段,修改其他属性不会影响键的排序结果,有两种合法方案可以实现需求:
方案1:用mutable修饰非比较属性
将不参与键比较的可修改成员标记为mutable,这类成员允许在const对象中被修改,同时将addItem方法标记为const即可:
class Bucket : Item { public: // 其他成员不变 void addItem(std::shared_ptr<Item> item) const { this->weight += item->getWeight(); this->items.push_back(item); } private: std::string type; mutable std::vector<std::shared_ptr<Item>> items; mutable int weight = 0; };
该方案改动最小,性能无额外损耗,只要确保参与比较的id永远不被修改即可。
方案2:使用C++17的extract方法操作节点
如果不想修改类的定义,可以用std::map的extract接口取出节点,修改键后再插回容器,因为id不变,插回后不会改变容器结构,开销极低:
int main(){ map<Bucket, vector<shared_ptr<Item>>> map; for(auto itr = map.begin(); itr != map.end(); ){ auto node = map.extract(itr++); for_each(node.mapped().begin(), node.mapped().end(), [&node](shared_ptr<Item> item){ node.key().addItem(item); }); map.insert(std::move(node)); } }
另外注意你原代码中的for_each第二个参数误写为begin,需修改为end才能正常遍历整个vector。
内容的提问来源于stack exchange,提问作者L.Gashi

