C++ 使用map提取和重新插入的限制性规则的基本原理
自C++17以来,支持提取节点并将其重新插入(可能插入到相同类型的另一个容器中)。C++ 使用map提取和重新插入的限制性规则的基本原理,c++,language-lawyer,c++17,undefined-behavior,C++,Language Lawyer,C++17,Undefined Behavior,自C++17以来,支持提取节点并将其重新插入(可能插入到相同类型的另一个容器中)。extract(key)返回的对象是a,它是仅移动的,并且对于地图容器具有成员函数 key_type &key() const; mapped_type &mapped() const; 它不仅允许修改映射类型,还允许修改密钥。这可用于在不重新分配的情况下更改键(示例取自): 编译(使用gcc 8.2.0使用-std=c++17)并给出输出 initially: address=0x7f
extract(key)
返回的对象是a,它是仅移动的,并且对于地图容器具有成员函数
key_type &key() const;
mapped_type &mapped() const;
它不仅允许修改映射类型,还允许修改密钥。这可用于在不重新分配的情况下更改键(示例取自):
编译(使用gcc 8.2.0使用-std=c++17
)并给出输出
initially: address=0x7f9e06c02738 value=papaya
after extract: address=0x7f9e06c02738 value=papaya
after insert: address=0x7f9e06c02738 value=papaya
正如预期的那样(对于std::string
代替固定字符串和/或无序映射
而不是映射
,获得了相同的结果)
编辑
请注意,我没有询问与修改键相关的问题(map
存储对)
我的问题仅仅是关于通过指针或引用访问映射元素的限制。只有当元素未被移动/复制,即其地址始终有效(事实上由标准指定)时,提取和插入习惯用法的整个思想才有意义。在提取状态下呈现对元素的访问看起来很奇怪,使得提取和插入机制变得不那么有用:考虑多线程代码,其中一个线程访问元素,而另一个线程提取并重新插入元素。这可以在没有任何问题的情况下实现,但可能会调用UB--为什么?
下面是一个UB场景(IMHO非常好,不需要UB):
void somefunc(object*ptr){ptr->do_something();}
无效重新设置键(映射与维护、int-oldKey、int-newKey)
{
如果(M.find(0)!=M.end()&&M.find(newKey)==M.end()){
自动手柄=M.extract(0);
handle.key()=newKey;
插入(标准::移动(手柄));
}
}
map M=fillMap();
自动ptr=addressof(M[0]);//取首字母地址
线程t1(somefunc,ptr);//使用所述地址访问对象
螺纹t2(重新编号,M,7);//提取和插入对象
当然,如果insert()。这是显而易见的,但用户可以对此有所了解。我认为“提取”系统中最主要的微妙之处在于映射的值类型是对-请注意常数
修改常量对象会导致未定义的行为,因此您需要非常小心,不要修改已知为常量的对象。当节点是任何映射的一部分时,键是const。提取机制的“魔力”(以及它花了这么长时间指定的原因)在于,当节点被提取时,密钥不是常量
这基本上要求您认真研究问题,并说服自己,pair
有时可以解释为pair
(请记住,pair
是允许用户专门化的模板!)。因此,为了避免修改const对象,必须对任何节点的插入和提取状态进行清晰的排序
[container.node.overview]p4中有标准的措辞来帮助解决专门化问题:
如果对
或对
存在用户定义的对
专门化,其中键
是
容器的key\u类型
和T
是容器的mapped\u类型
,涉及节点句柄的操作行为未定义
我只是跟进Kerrek SB的回答,希望能更详细地解释这个问题(因此更令人信服)。所采用的方法提到了std::pair
vsstd::pair
难题,并且“使用与std::launder
在提取和重新插入时使用的技术类似的技术,可以安全地实现两者之间的转换。”
这样,只要用户代码遵守您提到的限制,提取和重新插入就可以通过调用“实现‘魔术’”(这是本文中明确的措辞)来解决容器代码本身中任何可能的别名问题,从而避免与类型双关相关的优化
这就提出了一个问题,为什么不能将这种“魔力”扩展到包括这样的情况:用户代码通过一个指针访问分离节点的映射的
元素,而该指针是在节点仍然属于容器时获得的。原因是实现这种“魔术”的范围远远大于实现仅适用于节点提取和插入的有限魔术的范围
例如,考虑以下函数:
int f(std::pair<const int, int> &a, const std::pair<int, int> &b)
{
a.second = 5;
return b.second;
}
由于类型双关限制,显然这是UB。等等,我知道你想抗议是因为:
std::map
的节点类型
没有value()
方法
即使它这样做了,node\u type
也应该std::launder
(或大致相当的值)
然而,这些要点并没有提供实际的补救办法。就第一点而言,考虑这个小的变化:
int f(std::pair<const int, int> &a, const int &b)
{
a.second = 5;
return b;
}
int g()
{
std::map<int, int> m{{1, 1}};
auto &r = m[1];
auto node = m.extract(1);
return f(r, node.mapped());
}
这是因为使用std::launder
根本不授予用户键入双关语的权限。相反,std::launder
只允许通过在最初位于&value
的float
和位于std::launder
之后的int
之间建立生存期依赖关系来重用value
的内存。事实上,就标准而言,value
和*intp
不可能同时处于活动状态,因为它们具有指针不兼容的类型和相同的内存位置
(什么<代码>标准::流槽
void somefunc(object*ptr) { ptr->do_something(); }
void re_key(map<int,object> &M, int oldKey, int newKey)
{
if(M.find(0)!=M.end() && M.find(newKey)==M.end()) {
auto handle = M.extract(0);
handle.key() = newKey;
M.insert(std::move(handle));
}
}
map<int,object> M = fillMap();
auto ptr = addressof(M[0]); // takes initial address
thread t1(somefunc,ptr); // uses said address to access object
thread t2(re_key,M,7); // extracts and inserts an object
int f(std::pair<const int, int> &a, const std::pair<int, int> &b)
{
a.second = 5;
return b.second;
}
int g()
{
std::map<int, int> m{{1, 1}};
auto &r = m[1];
auto node = m.extract(1);
return f(r, node.value());
}
int f(std::pair<const int, int> &a, const int &b)
{
a.second = 5;
return b;
}
int g()
{
std::map<int, int> m{{1, 1}};
auto &r = m[1];
auto node = m.extract(1);
return f(r, node.mapped());
}
float g()
{
float value = 0.0f; // deliberate separate initialization, see below
value = 3.14f;
int *intp = std::launder(reinterpret_cast<int *>(&value));
*intp = 1;
return value + *intp;
}